You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by pk...@apache.org on 2024/10/22 15:26:40 UTC
(logging-log4j-transform) branch feature/converter-plugins-cache updated (72cba41 -> 41b503e)
This is an automated email from the ASF dual-hosted git repository.
pkarwasz pushed a change to branch feature/converter-plugins-cache
in repository https://gitbox.apache.org/repos/asf/logging-log4j-transform.git
discard 72cba41 Add a tool to manipulate plugin descriptors
new 41b503e Add a tool to manipulate plugin descriptors
This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version. This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:
* -- * -- B -- O -- O -- O (72cba41)
\
N -- N -- N refs/heads/feature/converter-plugins-cache (41b503e)
You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.
Any revisions marked "omit" are not gone; other references still
refer to them. Any revisions marked "discard" are gone forever.
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
log4j-codegen/.picocli-application-activator | 2 +-
log4j-converter-plugin-descriptor/.picocli-application-activator | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
(logging-log4j-transform) 01/01: Add a tool to manipulate plugin descriptors
Posted by pk...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
pkarwasz pushed a commit to branch feature/converter-plugins-cache
in repository https://gitbox.apache.org/repos/asf/logging-log4j-transform.git
commit 41b503e4b7fc13b866a65764057fe70f1ddce12a
Author: Piotr P. Karwasz <pi...@karwasz.org>
AuthorDate: Tue Oct 22 17:23:39 2024 +0200
Add a tool to manipulate plugin descriptors
The tool is based on picocli and supports the following commands:
* `toJson`: converts a `Log4j2Plugins.dat` to a JSON representation.
* `fromJson`: converts the JSON representation of a plugin descriptor to its `Log4j2Plugins.dat` form.
* `filterReflectConfig`: filters a GraalVM `reflect-config.json` file by removing the classes that are not contained in a `Log4j2Plugins.json` file.
---
log4j-codegen/.picocli-application-activator | 17 +
log4j-codegen/pom.xml | 68 +---
.../.picocli-application-activator | 17 +
log4j-converter-plugin-descriptor/pom.xml | 69 ++++
.../converter/plugins/PluginCacheConverter.java | 201 ++++++++++
.../converter/plugins/internal/JacksonUtils.java | 53 +++
.../plugins/internal/PluginDescriptors.java | 443 +++++++++++++++++++++
.../plugins/internal/ReflectConfigFilter.java | 58 +++
.../log4j/converter/plugins/package-info.java | 20 +
.../src/main/resources/Log4j2Plugins.schema.json | 61 +++
log4j-transform-parent/pom.xml | 73 ++++
pom.xml | 7 +
12 files changed, 1020 insertions(+), 67 deletions(-)
diff --git a/log4j-codegen/.picocli-application-activator b/log4j-codegen/.picocli-application-activator
new file mode 100644
index 0000000..a395748
--- /dev/null
+++ b/log4j-codegen/.picocli-application-activator
@@ -0,0 +1,17 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to you under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+This file activates the `picocli` profile
diff --git a/log4j-codegen/pom.xml b/log4j-codegen/pom.xml
index 2d87ba4..c53f6a7 100644
--- a/log4j-codegen/pom.xml
+++ b/log4j-codegen/pom.xml
@@ -61,11 +61,10 @@
<scope>provided</scope>
</dependency>
- <!-- Compile dependencies: the artifact is shaded, so limit these to the maximum -->
+ <!-- Compile dependencies: the artifact is shaded, so limit these. -->
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
- <version>${picocli.version}</version>
</dependency>
<!-- Test dependencies -->
@@ -89,69 +88,4 @@
</dependencies>
- <build>
-
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-compiler-plugin</artifactId>
- <configuration>
- <annotationProcessorPaths combine.children="append">
- <path>
- <groupId>info.picocli</groupId>
- <artifactId>picocli-codegen</artifactId>
- <version>${picocli.version}</version>
- </path>
- </annotationProcessorPaths>
- </configuration>
- </plugin>
-
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-shade-plugin</artifactId>
- <version>${maven-shade-plugin.version}</version>
- <dependencies>
- <dependency>
- <groupId>org.apache.logging.log4j</groupId>
- <artifactId>log4j-transform-maven-shade-plugin-extensions</artifactId>
- <version>${project.version}</version>
- </dependency>
- </dependencies>
- <executions>
- <execution>
- <id>shade-jar-with-dependencies</id>
- <goals>
- <goal>shade</goal>
- </goals>
- <configuration>
- <filters>
- <filter>
- <artifact>*:*</artifact>
- <excludes>
- <exclude>module-info.class</exclude>
- <exclude>META-INF/versions/*/module-info.class</exclude>
- <exclude>META-INF/MANIFEST.MF</exclude>
- </excludes>
- </filter>
- </filters>
- <shadedArtifactAttached>true</shadedArtifactAttached>
- <shadedClassifierName>shaded</shadedClassifierName>
- <transformers>
- <transformer implementation="org.apache.maven.plugins.shade.resource.ApacheLicenseResourceTransformer" />
- <transformer implementation="org.apache.maven.plugins.shade.resource.ApacheNoticeResourceTransformer" />
- <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
- <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
- <manifestEntries>
- <Multi-Release>true</Multi-Release>
- </manifestEntries>
- </transformer>
- </transformers>
- </configuration>
- </execution>
- </executions>
- </plugin>
- </plugins>
-
- </build>
-
</project>
diff --git a/log4j-converter-plugin-descriptor/.picocli-application-activator b/log4j-converter-plugin-descriptor/.picocli-application-activator
new file mode 100644
index 0000000..a395748
--- /dev/null
+++ b/log4j-converter-plugin-descriptor/.picocli-application-activator
@@ -0,0 +1,17 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to you under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+This file activates the `picocli` profile
diff --git a/log4j-converter-plugin-descriptor/pom.xml b/log4j-converter-plugin-descriptor/pom.xml
new file mode 100644
index 0000000..625bc23
--- /dev/null
+++ b/log4j-converter-plugin-descriptor/pom.xml
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one or more
+ ~ contributor license agreements. See the NOTICE file distributed with
+ ~ this work for additional information regarding copyright ownership.
+ ~ The ASF licenses this file to you under the Apache License, Version 2.0
+ ~ (the "License"); you may not use this file except in compliance with
+ ~ the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-transform-parent</artifactId>
+ <version>${revision}</version>
+ <relativePath>../log4j-transform-parent</relativePath>
+ </parent>
+
+ <artifactId>log4j-converter-plugins-cache</artifactId>
+ <name>Apache Log4j plugins cache to JSON converter</name>
+ <description>Converts `Log4j2Plugins.dat` files from and to JSON.</description>
+
+ <properties>
+ <!-- Disabling `bnd-baseline-maven-plugin`, since we don't have a release yet to compare against. -->
+ <bnd.baseline.fail.on.missing>false</bnd.baseline.fail.on.missing>
+
+ <Main-Class>org.apache.logging.log4j.converter.plugins.PluginCacheConverter</Main-Class>
+
+ <!-- Dependency versions -->
+ <jackson.version>2.18.0</jackson.version>
+ </properties>
+
+ <dependencies>
+
+ <dependency>
+ <groupId>org.jspecify</groupId>
+ <artifactId>jspecify</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ <!-- Compile dependencies: the artifact is shaded, so limit these. -->
+ <dependency>
+ <groupId>info.picocli</groupId>
+ <artifactId>picocli</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-core</artifactId>
+ <version>${jackson.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ <version>${jackson.version}</version>
+ </dependency>
+
+ </dependencies>
+
+</project>
diff --git a/log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/PluginCacheConverter.java b/log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/PluginCacheConverter.java
new file mode 100644
index 0000000..034af32
--- /dev/null
+++ b/log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/PluginCacheConverter.java
@@ -0,0 +1,201 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.converter.plugins;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import org.apache.logging.log4j.converter.plugins.internal.PluginDescriptors.Namespace;
+import org.apache.logging.log4j.converter.plugins.internal.PluginDescriptors.PluginDescriptor;
+import org.apache.logging.log4j.converter.plugins.internal.ReflectConfigFilter;
+import org.jspecify.annotations.Nullable;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Parameters;
+
+@Command(name = "convert")
+public class PluginCacheConverter {
+
+ private static final JsonFactory jsonFactory = new ObjectMapper().getFactory();
+
+ public static void main(final String[] args) {
+ System.exit(new CommandLine(new PluginCacheConverter()).execute(args));
+ }
+
+ @Command
+ public void toJson(
+ @Parameters(description = "Classpath containing Log4j Core plugins") final String classPath,
+ @Parameters(description = "Output file", defaultValue = "Log4j2Plugins.json") final File outputFile)
+ throws IOException {
+ new PluginDescriptorToJsonConverter(classPath, outputFile).call();
+ }
+
+ @Command
+ public void fromJson(
+ @Parameters(description = "Input JSON file") final File input,
+ @Parameters(description = "Output `Log4j2Plugins.dat` file", defaultValue = "Log4j2Plugins.dat")
+ final File output)
+ throws IOException {
+ new JsonToPluginDescriptorConverter(input, output).call();
+ }
+
+ @Command
+ public void filterReflectConfig(
+ @Parameters(description = "Plugin descriptor (as JSON)") final File pluginDescriptor,
+ @Parameters(description = "Reflection configuration") final File reflectConfigInput)
+ throws IOException {
+ new ReflectConfigTransformer(pluginDescriptor, reflectConfigInput).call();
+ }
+
+ private static final class PluginDescriptorToJsonConverter implements Callable<@Nullable Void> {
+
+ private static final String PLUGIN_CACHE_FILE =
+ "META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat";
+
+ private final ClassLoader classLoader;
+
+ private final File outputFile;
+
+ public PluginDescriptorToJsonConverter(final String classPath, final File outputFile) {
+ final URL[] urls = validateInput(classPath);
+ this.classLoader =
+ AccessController.doPrivileged((PrivilegedAction<ClassLoader>) () -> new URLClassLoader(urls));
+ this.outputFile = outputFile;
+ }
+
+ @Override
+ public @Nullable Void call() throws IOException {
+ try (final OutputStream output = Files.newOutputStream(outputFile.toPath());
+ final JsonGenerator generator = jsonFactory.createGenerator(output)) {
+ final PluginDescriptor pluginDescriptor = new PluginDescriptor();
+ // Read all `Log4j2Plugins.dat` file on the classpath
+ for (final URL url : Collections.list(classLoader.getResources(PLUGIN_CACHE_FILE))) {
+ try (final BufferedInputStream input = new BufferedInputStream(url.openStream())) {
+ pluginDescriptor.readPluginDescriptor(input);
+ }
+ }
+ // Write JSON file
+ pluginDescriptor.withBuilderHierarchy(classLoader).toJson(generator);
+ }
+ return null;
+ }
+
+ private static URL[] validateInput(final String classPath) {
+ final String[] classPathElements = classPath.split(File.pathSeparator, -1);
+ return Arrays.stream(classPathElements)
+ .map(classPathElement -> {
+ final Path inputPath = Paths.get(classPathElement);
+ if (!Files.isRegularFile(inputPath)) {
+ throw new IllegalArgumentException("Input file " + inputPath + " is not a file.");
+ }
+ if (!inputPath.getFileName().toString().endsWith(".jar")) {
+ throw new IllegalArgumentException(
+ "Invalid input file, only JAR files are supported: " + inputPath);
+ }
+ try {
+ return inputPath.toUri().toURL();
+ } catch (final MalformedURLException e) {
+
+ throw new IllegalArgumentException(
+ "Innput file " + inputPath + " can not be accessed as URL.", e);
+ }
+ })
+ .toArray(URL[]::new);
+ }
+ }
+
+ private static class JsonToPluginDescriptorConverter implements Callable<@Nullable Void> {
+
+ private final Path input;
+ private final Path output;
+
+ public JsonToPluginDescriptorConverter(final File input, final File output) {
+ this.input = input.toPath();
+ this.output = output.toPath();
+ }
+
+ @Override
+ public @Nullable Void call() throws IOException {
+ try (final InputStream inputStream = Files.newInputStream(input);
+ final JsonParser parser = jsonFactory.createParser(inputStream);
+ final OutputStream outputStream = Files.newOutputStream(output)) {
+ final PluginDescriptor pluginDescriptor = new PluginDescriptor();
+ // Input JSON
+ pluginDescriptor.readJson(parser);
+ // Output `Log4j2Plugins.dat` file
+ pluginDescriptor.toPluginDescriptor(outputStream);
+ }
+ return null;
+ }
+ }
+
+ private static class ReflectConfigTransformer implements Callable<@Nullable Void> {
+
+ private final Path pluginDescriptorPath;
+ private final Path input;
+ private final Path output;
+
+ public ReflectConfigTransformer(final File pluginDescriptorPath, final File input) {
+ this.pluginDescriptorPath = pluginDescriptorPath.toPath();
+ this.input = input.toPath();
+ this.output = Paths.get("reflect-config.json");
+ }
+
+ @Override
+ public @Nullable Void call() throws IOException {
+ final PluginDescriptor pluginDescriptor = new PluginDescriptor();
+ // Read plugin descriptor
+ try (final InputStream inputStream = Files.newInputStream(pluginDescriptorPath);
+ final JsonParser parser = jsonFactory.createParser(inputStream)) {
+ pluginDescriptor.readJson(parser);
+ }
+ // Find all referenced classes
+ final Set<String> classNames = new HashSet<>();
+ pluginDescriptor.getNamespaces().flatMap(Namespace::getPlugins).forEach(p -> {
+ classNames.add(p.getClassName());
+ classNames.addAll(p.getBuilderHierarchy());
+ });
+ final ReflectConfigFilter filter = new ReflectConfigFilter(classNames);
+ try (final InputStream inputStream = Files.newInputStream(input);
+ final JsonParser parser = jsonFactory.createParser(inputStream);
+ final OutputStream outputStream = Files.newOutputStream(output);
+ final JsonGenerator generator = jsonFactory.createGenerator(outputStream)) {
+ filter.filter(parser, generator);
+ }
+ return null;
+ }
+ }
+}
diff --git a/log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/internal/JacksonUtils.java b/log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/internal/JacksonUtils.java
new file mode 100644
index 0000000..412dd90
--- /dev/null
+++ b/log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/internal/JacksonUtils.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.converter.plugins.internal;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import java.io.IOException;
+
+final class JacksonUtils {
+
+ static void assertArrayStart(final JsonParser parser) throws IOException {
+ parser.nextToken();
+ assertCurrentToken(parser, JsonToken.START_ARRAY, "start of JSON array");
+ }
+
+ static void assertArrayEnd(final JsonParser parser) throws IOException {
+ assertCurrentToken(parser, JsonToken.END_ARRAY, "end of JSON array");
+ }
+
+ static void assertObjectStart(final JsonParser parser) throws IOException {
+ parser.nextToken();
+ assertCurrentToken(parser, JsonToken.START_OBJECT, "start of JSON object");
+ }
+
+ static void assertObjectEnd(final JsonParser parser) throws IOException {
+ assertCurrentToken(parser, JsonToken.END_OBJECT, "end of JSON object");
+ }
+
+ static void assertCurrentToken(final JsonParser parser, final JsonToken token, final String expectingMessage)
+ throws IOException {
+ if (parser.currentToken() != token) {
+ throw new IOException(String.format(
+ "Parser error at %s: expecting %s, but found '%s'.",
+ parser.currentLocation(), expectingMessage, parser.currentName()));
+ }
+ }
+
+ private JacksonUtils() {}
+}
diff --git a/log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/internal/PluginDescriptors.java b/log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/internal/PluginDescriptors.java
new file mode 100644
index 0000000..50638b1
--- /dev/null
+++ b/log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/internal/PluginDescriptors.java
@@ -0,0 +1,443 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.converter.plugins.internal;
+
+import static org.apache.logging.log4j.converter.plugins.internal.JacksonUtils.assertArrayEnd;
+import static org.apache.logging.log4j.converter.plugins.internal.JacksonUtils.assertArrayStart;
+import static org.apache.logging.log4j.converter.plugins.internal.JacksonUtils.assertObjectEnd;
+import static org.apache.logging.log4j.converter.plugins.internal.JacksonUtils.assertObjectStart;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import java.io.DataInput;
+import java.io.DataInputStream;
+import java.io.DataOutput;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.stream.Stream;
+
+public final class PluginDescriptors {
+
+ private static final String PLUGIN_BUILDER_FACTORY =
+ "org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory";
+ private static final String PLUGIN_FACTORY = "org.apache.logging.log4j.core.config.plugins.PluginFactory";
+
+ public static final class PluginDescriptor {
+ private final Map<String, Namespace> namespacesByName;
+
+ public PluginDescriptor() {
+ this(new TreeMap<>());
+ }
+
+ private PluginDescriptor(final Map<String, Namespace> namespacesByName) {
+ this.namespacesByName = namespacesByName;
+ }
+
+ /**
+ * Reads an additional plugin descriptor.
+ *
+ * @param input An input stream for a `Log4j2Plugins.dat` file.
+ */
+ public void readPluginDescriptor(final InputStream input) throws IOException {
+ try (final DataInputStream dataInput = new DataInputStream(input)) {
+ final int namespaceCount = dataInput.readInt();
+ for (int i = 0; i < namespaceCount; i++) {
+ final Namespace namespace = Namespace.fromDataInput(dataInput);
+ namespacesByName.merge(namespace.getName(), namespace, Namespace::merge);
+ }
+ }
+ }
+
+ public void toPluginDescriptor(final OutputStream output) throws IOException {
+ try (final DataOutputStream dataOutput = new DataOutputStream(output)) {
+ dataOutput.writeInt(namespacesByName.size());
+ for (final Namespace namespace : namespacesByName.values()) {
+ namespace.writeDataOutput(dataOutput);
+ }
+ }
+ }
+
+ public void readJson(final JsonParser parser) throws IOException {
+ assertObjectStart(parser);
+ while (parser.nextToken() == JsonToken.FIELD_NAME) {
+ final String namespace = parser.currentName();
+ namespacesByName.merge(namespace, Namespace.fromJson(parser, namespace), Namespace::merge);
+ }
+ assertObjectEnd(parser);
+ }
+
+ public PluginDescriptor withBuilderHierarchy(final ClassLoader classLoader) {
+ final Map<String, Namespace> namespacesByName = new TreeMap<>(this.namespacesByName);
+ namespacesByName.replaceAll((k, v) -> v.withBuilderHierarchy(classLoader));
+ return new PluginDescriptor(namespacesByName);
+ }
+
+ public void toJson(final JsonGenerator generator) throws IOException {
+ generator.writeStartObject();
+ for (final Namespace namespace : namespacesByName.values()) {
+ generator.writeFieldName(namespace.getName());
+ namespace.toJson(generator);
+ }
+ generator.writeEndObject();
+ }
+
+ public Stream<Namespace> getNamespaces() {
+ return namespacesByName.values().stream();
+ }
+ }
+
+ /**
+ * Represents a namespace of plugins
+ */
+ public static final class Namespace {
+
+ private final String name;
+
+ private final Map<String, Plugin> pluginsByClassName;
+
+ private Namespace(final String name, final Map<String, Plugin> pluginsByClassName) {
+ this.name = name;
+ this.pluginsByClassName = pluginsByClassName;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Stream<Plugin> getPlugins() {
+ return pluginsByClassName.values().stream();
+ }
+
+ static Namespace fromDataInput(final DataInput dataInput) throws IOException {
+ final Map<String, Plugin> pluginsByClassName = new TreeMap<>();
+ final String namespace = dataInput.readUTF();
+ final int pluginCount = dataInput.readInt();
+ for (int i = 0; i < pluginCount; i++) {
+ final Plugin plugin = Plugin.fromDataInput(dataInput);
+ pluginsByClassName.merge(plugin.getClassName(), plugin, Plugin::merge);
+ }
+ return new Namespace(namespace, Collections.unmodifiableMap(pluginsByClassName));
+ }
+
+ void writeDataOutput(final DataOutput dataOutput) throws IOException {
+ dataOutput.writeUTF(name);
+ final int pluginCount = countPluginNames(pluginsByClassName.values());
+ dataOutput.writeInt(pluginCount);
+ for (final Plugin plugin : pluginsByClassName.values()) {
+ plugin.toDataOutput(dataOutput);
+ }
+ }
+
+ static Namespace fromJson(final JsonParser parser, final String namespace) throws IOException {
+ final Map<String, Plugin> pluginsByClassName = new TreeMap<>();
+ assertObjectStart(parser);
+ while (parser.nextToken() == JsonToken.FIELD_NAME) {
+ final String className = parser.currentName();
+ final Plugin plugin = Plugin.fromJson(parser, className);
+ pluginsByClassName.put(className, plugin);
+ }
+ assertObjectEnd(parser);
+ return new Namespace(namespace, Collections.unmodifiableMap(pluginsByClassName));
+ }
+
+ void toJson(final JsonGenerator generator) throws IOException {
+ generator.writeStartObject();
+ for (final Plugin plugin : pluginsByClassName.values()) {
+ generator.writeFieldName(plugin.getClassName());
+ plugin.toJson(generator);
+ }
+ generator.writeEndObject();
+ }
+
+ Namespace merge(final Namespace other) {
+ final Map<String, Plugin> pluginsByClassName = new TreeMap<>(this.pluginsByClassName);
+ pluginsByClassName.putAll(other.pluginsByClassName);
+ return new Namespace(name, Collections.unmodifiableMap(pluginsByClassName));
+ }
+
+ Namespace withBuilderHierarchy(final ClassLoader classLoader) {
+ final Map<String, Plugin> pluginsByClassName = new TreeMap<>(this.pluginsByClassName);
+ pluginsByClassName.replaceAll((k, v) -> v.withBuilderHierarchy(classLoader));
+ return new Namespace(name, Collections.unmodifiableMap(pluginsByClassName));
+ }
+
+ private int countPluginNames(final Collection<Plugin> plugins) {
+ return plugins.stream()
+ .mapToInt(plugin -> plugin.getPluginNames().size())
+ .sum();
+ }
+ }
+
+ /**
+ * Represents a single plugin.
+ */
+ public static final class Plugin {
+
+ private static final String BUILDER_HIERARCHY = "builderHierarchy";
+ private static final String PLUGIN_NAMES = "pluginNames";
+ private static final String ELEMENT_NAME = "elementName";
+ private static final String PRINTABLE = "printable";
+ private static final String DEFER = "defer";
+
+ /**
+ * The name and aliases of the plugin.
+ */
+ private final Set<String> pluginNames;
+
+ /**
+ * The name and all the plugin aliases.
+ */
+ private final String elementName;
+
+ /**
+ * The name of the plugin class.
+ * <p>
+ * Only present in the serialized representation.
+ * In JSON the className is encoded as key.
+ * </p>
+ */
+ private final String className;
+
+ private final boolean printable;
+
+ private final boolean defer;
+
+ /**
+ * The fully qualified class name of the builder and its ancestor.
+ * <p>
+ * Only present in the JSON representation.
+ * </p>
+ */
+ private final List<String> builderHierarchy;
+
+ private Plugin(
+ final Set<String> pluginNames,
+ final String elementName,
+ final String className,
+ final boolean printable,
+ final boolean defer,
+ final List<String> builderHierarchy) {
+ this.pluginNames = pluginNames;
+ this.elementName = elementName;
+ this.className = className;
+ this.printable = printable;
+ this.defer = defer;
+ this.builderHierarchy = builderHierarchy;
+ }
+
+ public Set<String> getPluginNames() {
+ return pluginNames;
+ }
+
+ public String getClassName() {
+ return className;
+ }
+
+ public List<String> getBuilderHierarchy() {
+ return builderHierarchy;
+ }
+
+ static Plugin fromDataInput(final DataInput dataInput) throws IOException {
+ final String pluginName = dataInput.readUTF();
+ final String className = dataInput.readUTF();
+ final String elementName = dataInput.readUTF();
+ final boolean printable = dataInput.readBoolean();
+ final boolean defer = dataInput.readBoolean();
+ return new Plugin(
+ Collections.singleton(pluginName),
+ elementName,
+ className,
+ printable,
+ defer,
+ Collections.emptyList());
+ }
+
+ void toDataOutput(final DataOutput dataOutput) throws IOException {
+ for (final String pluginName : pluginNames) {
+ dataOutput.writeUTF(pluginName);
+ dataOutput.writeUTF(className);
+ dataOutput.writeUTF(elementName);
+ dataOutput.writeBoolean(printable);
+ dataOutput.writeBoolean(defer);
+ }
+ }
+
+ static Plugin fromJson(final JsonParser parser, final String className) throws IOException {
+ final Set<String> pluginNames = new TreeSet<>();
+ final List<String> builderHierarchy = new ArrayList<>();
+ String elementName = null;
+ boolean printable = false, defer = false;
+ assertObjectStart(parser);
+ while (parser.nextToken() == JsonToken.FIELD_NAME) {
+ switch (parser.currentName()) {
+ case PLUGIN_NAMES:
+ readArray(parser, pluginNames);
+ break;
+ case BUILDER_HIERARCHY:
+ // read and ignore
+ readArray(parser, builderHierarchy);
+ break;
+ case ELEMENT_NAME:
+ elementName = parser.nextTextValue();
+ break;
+ case DEFER:
+ defer = parser.nextBooleanValue();
+ break;
+ case PRINTABLE:
+ printable = parser.nextBooleanValue();
+ break;
+ default:
+ throw new IOException(
+ "Unknown property " + parser.currentName() + " for Plugin element " + className);
+ }
+ }
+ assertObjectEnd(parser);
+ return new Plugin(pluginNames, elementName, className, printable, defer, builderHierarchy);
+ }
+
+ void toJson(final JsonGenerator generator) throws IOException {
+ generator.writeStartObject();
+ // Aliases
+ generator.writeArrayFieldStart(PLUGIN_NAMES);
+ for (final String pluginName : pluginNames) {
+ generator.writeString(pluginName);
+ }
+ generator.writeEndArray();
+ // Simple fields
+ generator.writeStringField(ELEMENT_NAME, elementName);
+ generator.writeBooleanField(PRINTABLE, printable);
+ generator.writeBooleanField(DEFER, defer);
+ // Compute the class name of the builder
+ generator.writeArrayFieldStart(BUILDER_HIERARCHY);
+ for (final String fqcn : builderHierarchy) {
+ generator.writeString(fqcn);
+ }
+ generator.writeEndArray();
+ generator.writeEndObject();
+ }
+
+ private Plugin merge(final Plugin other) {
+ // Intentionally ignore `elementName`
+ if (!this.className.equals(other.className)
+ || this.printable != other.printable
+ || this.defer != other.defer) {
+ throw new IllegalArgumentException(
+ "Attempting to merge incompatible plugins: " + this + " and " + other);
+ }
+ final Set<String> pluginNames = new TreeSet<>(this.pluginNames);
+ pluginNames.addAll(other.pluginNames);
+ return new Plugin(
+ Collections.unmodifiableSet(pluginNames),
+ this.elementName,
+ this.className,
+ this.printable,
+ this.defer,
+ this.builderHierarchy);
+ }
+
+ /**
+ * Computes the builder hierarchy.
+ *
+ * @param classLoader Class loader to use to load classes.
+ * @return A new plugin with the computed builder hierarchy.
+ */
+ public Plugin withBuilderHierarchy(final ClassLoader classLoader) {
+ final List<String> builderHierarchy =
+ Collections.unmodifiableList(findBuilderClassHierarchy(classLoader, className));
+ return new Plugin(pluginNames, elementName, className, printable, defer, builderHierarchy);
+ }
+
+ @Override
+ public String toString() {
+ return "Plugin{" + "pluginNames="
+ + pluginNames + ", elementName='"
+ + elementName + '\'' + ", className='"
+ + className + '\'' + ", printable="
+ + printable + ", defer="
+ + defer + ", builderHierarchy="
+ + builderHierarchy + '}';
+ }
+
+ private static List<String> findBuilderClassHierarchy(final ClassLoader classLoader, final String className) {
+ try {
+ final Class<?> pluginClass = classLoader.loadClass(className);
+ for (final Method method : pluginClass.getMethods()) {
+ for (final Annotation annotation : method.getAnnotations()) {
+ switch (annotation.annotationType().getName()) {
+ case PLUGIN_FACTORY:
+ // No need to search further, there is no builder
+ return Collections.emptyList();
+ case PLUGIN_BUILDER_FACTORY:
+ return computeClassHierarchy(findBuilderClass(method.getGenericReturnType()));
+ }
+ }
+ }
+ return Collections.emptyList();
+ } catch (final ClassNotFoundException e) {
+ throw new IllegalArgumentException("Unable to find class " + className, e);
+ }
+ }
+
+ private static Class<?> findBuilderClass(final Type type) {
+ if (type instanceof Class) {
+ return ((Class<?>) type);
+ }
+ if (type instanceof ParameterizedType) {
+ return findBuilderClass(((ParameterizedType) type).getRawType());
+ }
+ if (type instanceof TypeVariable) {
+ return findBuilderClass(((TypeVariable<?>) type).getBounds()[0]);
+ }
+ throw new IllegalArgumentException("Unable to handle reflective type: " + type);
+ }
+
+ private static List<String> computeClassHierarchy(final Class<?> clazz) {
+ final List<String> classes = new ArrayList<>();
+ Class<?> current = clazz;
+ while (current != null && !current.equals(Object.class)) {
+ classes.add(current.getName());
+ current = current.getSuperclass();
+ }
+ return classes;
+ }
+
+ private static void readArray(final JsonParser parser, final Collection<? super String> output)
+ throws IOException {
+ assertArrayStart(parser);
+ while (parser.nextToken() == JsonToken.VALUE_STRING) {
+ output.add(parser.getText());
+ }
+ assertArrayEnd(parser);
+ }
+ }
+}
diff --git a/log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/internal/ReflectConfigFilter.java b/log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/internal/ReflectConfigFilter.java
new file mode 100644
index 0000000..73a38d8
--- /dev/null
+++ b/log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/internal/ReflectConfigFilter.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.converter.plugins.internal;
+
+import static org.apache.logging.log4j.converter.plugins.internal.JacksonUtils.assertArrayEnd;
+import static org.apache.logging.log4j.converter.plugins.internal.JacksonUtils.assertArrayStart;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.databind.JsonNode;
+import java.io.IOException;
+import java.util.Set;
+
+public class ReflectConfigFilter {
+
+ private static final String TYPE_NAME = "name";
+
+ private final Set<String> includeClassNames;
+
+ public ReflectConfigFilter(final Set<String> includeClassNames) {
+ this.includeClassNames = includeClassNames;
+ }
+
+ public void filter(final JsonParser input, final JsonGenerator output) throws IOException {
+ assertArrayStart(input);
+ output.writeStartArray();
+ // Read and filter entries
+ while (input.nextToken() == JsonToken.START_OBJECT) {
+ final JsonNode node = input.readValueAsTree();
+ final JsonNode nameNode = node.get(TYPE_NAME);
+ if (nameNode != null) {
+ final String name = nameNode.asText();
+ // Include all the plugin visitors
+ if (name.startsWith("org.apache.logging.log4j.core.config.plugins.visitors")
+ || includeClassNames.contains(name)) {
+ output.writeTree(node);
+ }
+ }
+ }
+ assertArrayEnd(input);
+ output.writeEndArray();
+ }
+}
diff --git a/log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/package-info.java b/log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/package-info.java
new file mode 100644
index 0000000..8a16a06
--- /dev/null
+++ b/log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@NullMarked
+package org.apache.logging.log4j.converter.plugins;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/log4j-converter-plugin-descriptor/src/main/resources/Log4j2Plugins.schema.json b/log4j-converter-plugin-descriptor/src/main/resources/Log4j2Plugins.schema.json
new file mode 100644
index 0000000..d17d078
--- /dev/null
+++ b/log4j-converter-plugin-descriptor/src/main/resources/Log4j2Plugins.schema.json
@@ -0,0 +1,61 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://logging.apache.org/json/schema/Log4j2Plugins.schema.json",
+ "title": "Log4j 2.x Plugin Descriptor",
+ "description": "A JSON version of the `Log4j2Plugins.dat` file.",
+ "type": "object",
+ "additionalProperties": {
+ "type": {
+ "$ref": "#/definitions/namespace"
+ }
+ },
+ "definitions": {
+ "plugin": {
+ "description": "Represents a Log4j Core Plugin.",
+ "type": "object",
+ "properties": {
+ "pluginNames": {
+ "description": "The name and aliases of the plugin in lowercase.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "elementName": {
+ "description": "The generic element name/category of the plugin.",
+ "type": "string"
+ },
+ "printable": {
+ "description": "Indicates if the plugin has a useful toString() method.",
+ "type": "boolean"
+ },
+ "defer": {
+ "description": "Indicates if the instantiation of configuration parameters should be deferred.",
+ "type": "boolean"
+ },
+ "builderHierarchy": {
+ "description": "The fully qualified name of the builder class and its ancestors.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "required": [
+ "pluginNames",
+ "elementName",
+ "printable",
+ "defer"
+ ]
+ },
+ "namespace": {
+ "description": "Represents a namespace of plugins. The key is the fully qualified class name of the plugin",
+ "type": "object",
+ "additionalProperties": {
+ "type": {
+ "$ref": "#/definitions/plugin"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/log4j-transform-parent/pom.xml b/log4j-transform-parent/pom.xml
index 31e48a0..6be8df5 100644
--- a/log4j-transform-parent/pom.xml
+++ b/log4j-transform-parent/pom.xml
@@ -40,6 +40,7 @@
<junit.version>5.11.3</junit.version>
<log4j.version>2.24.1</log4j.version>
<maven.version>3.9.9</maven.version>
+ <picocli.version>4.7.5</picocli.version>
<plexus-utils.version>4.0.2</plexus-utils.version>
<slf4j.version>2.0.16</slf4j.version>
@@ -120,6 +121,12 @@
<version>${maven.version}</version>
</dependency>
+ <dependency>
+ <groupId>info.picocli</groupId>
+ <artifactId>picocli</artifactId>
+ <version>${picocli.version}</version>
+ </dependency>
+
<dependency>
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-utils</artifactId>
@@ -162,6 +169,7 @@
</build>
<profiles>
+
<profile>
<id>java8-tests</id>
<properties>
@@ -181,6 +189,71 @@
</plugins>
</build>
</profile>
+
+ <!-- Profile for picocli-base applications -->
+ <profile>
+ <id>picocli</id>
+ <activation>
+ <file>
+ <exists>.picocli-application-activator</exists>
+ </file>
+ </activation>
+
+ <build>
+
+ <plugins>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <annotationProcessorPaths combine.children="append">
+ <path>
+ <groupId>info.picocli</groupId>
+ <artifactId>picocli-codegen</artifactId>
+ <version>${picocli.version}</version>
+ </path>
+ </annotationProcessorPaths>
+ </configuration>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-shade-plugin</artifactId>
+ <version>${maven-shade-plugin.version}</version>
+ <executions>
+ <execution>
+ <id>shade-jar-with-dependencies</id>
+ <goals>
+ <goal>shade</goal>
+ </goals>
+ <configuration>
+ <filters>
+ <filter>
+ <artifact>*:*</artifact>
+ <excludes>
+ <exclude>module-info.class</exclude>
+ <exclude>META-INF/versions/*/module-info.class</exclude>
+ <exclude>META-INF/MANIFEST.MF</exclude>
+ </excludes>
+ </filter>
+ </filters>
+ <shadedArtifactAttached>true</shadedArtifactAttached>
+ <shadedClassifierName>shaded</shadedClassifierName>
+ <transformers>
+ <transformer implementation="org.apache.maven.plugins.shade.resource.ApacheLicenseResourceTransformer" />
+ <transformer implementation="org.apache.maven.plugins.shade.resource.ApacheNoticeResourceTransformer" />
+ <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
+ <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer" />
+ </transformers>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+
</profiles>
</project>
diff --git a/pom.xml b/pom.xml
index e25d179..c13e522 100644
--- a/pom.xml
+++ b/pom.xml
@@ -74,6 +74,7 @@
<!-- Modules here must have a corresponding entry in `dependencyManagement > dependencies` block below! -->
<module>log4j-codegen</module>
+ <module>log4j-converter-plugin-descriptor</module>
<module>log4j-transform-maven-plugin</module>
<module>log4j-transform-maven-shade-plugin-extensions</module>
<module>log4j-weaver</module>
@@ -123,6 +124,12 @@
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-converter-plugin-descriptor</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-transform-maven-plugin</artifactId>