Add a tool to manipulate plugin descriptors (#139)
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.
diff --git a/.gitattributes b/.gitattributes
index 10914c1..597281d 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -13,8 +13,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-# Checked by Spotless (LF line endings)
-*.java text eol=lf
-*.xml text eol=lf
-*.yaml text eol=lf
-*.yml text eol=lf
+# All text files with LF line endings
+* text=auto eol=lf
+# Maven Wrapper cmd script
+/mvnw.cmd eol=crlf
+# Maven Wrapper need LF line endings
+/.mvn/wrapper/maven-wrapper.properties eol=lf
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..74d8e5d
--- /dev/null
+++ b/log4j-converter-plugin-descriptor/pom.xml
@@ -0,0 +1,125 @@
+<?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-plugin-descriptor</artifactId>
+ <name>Apache Log4j plugin descriptor tools</name>
+ <description>Tools to manipulate `Log4j2Plugins.dat` plugin descriptors and synchronize them with GraalVM reachability metadata.</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>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-api</artifactId>
+ </dependency>
+
+ <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>
+
+ <build>
+ <plugins>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-assembly-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>create-shaded-resources</id>
+ <goals>
+ <goal>single</goal>
+ </goals>
+ <phase>prepare-package</phase>
+ <configuration>
+ <inlineDescriptors>
+ <assembly>
+ <id>shaded-resources</id>
+ <formats>
+ <format>jar</format>
+ </formats>
+ <baseDirectory>/</baseDirectory>
+ <fileSets>
+ <fileSet>
+ <directory>src/main/shaded-resources</directory>
+ <outputDirectory>/</outputDirectory>
+ </fileSet>
+ </fileSets>
+ </assembly>
+ </inlineDescriptors>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-shade-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>shade-jar-with-dependencies</id>
+ <configuration>
+ <extraJars>
+ <jar>${project.build.directory}/${project.build.finalName}-shaded-resources.jar</jar>
+ </extraJars>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+
+ </plugins>
+ </build>
+</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..63b1ee3
--- /dev/null
+++ b/log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/PluginCacheConverter.java
@@ -0,0 +1,275 @@
+/*
+ * 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.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+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.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+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.Option;
+import picocli.CommandLine.Parameters;
+import picocli.CommandLine.ScopeType;
+
+@Command(name = "convertPlugin", mixinStandardHelpOptions = true)
+public class PluginCacheConverter {
+
+ private static final String PLUGIN_DESCRIPTOR_FILE =
+ "META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat";
+ private static final String PLUGIN_DESCRIPTOR_JSON_FILE = "Log4j2Plugins.json";
+
+ private static final Logger logger = LogManager.getLogger(PluginCacheConverter.class);
+ private static final JsonFactory jsonFactory = new ObjectMapper().getFactory();
+
+ @Option(
+ names = {"-o", "--outputDirectory"},
+ description = "Directory for the command output",
+ defaultValue = ".",
+ scope = ScopeType.INHERIT)
+ private File outputDirectory;
+
+ public static void main(final String[] args) {
+ System.exit(new CommandLine(new PluginCacheConverter()).execute(args));
+ }
+
+ @Command
+ public void toJson(
+ @Parameters(
+ arity = "1..*",
+ description = "Classpath containing Log4j Core plugins",
+ paramLabel = "<classPathElement>")
+ final List<String> classPath)
+ throws IOException {
+ new PluginDescriptorToJsonConverter(classPath, outputDirectory).call();
+ }
+
+ @Command
+ public void fromJson(@Parameters(description = "Plugin descriptor in JSON format") final File jsonPluginDescriptor)
+ throws IOException {
+ new JsonToPluginDescriptorConverter(jsonPluginDescriptor, outputDirectory).call();
+ }
+
+ @Command
+ public void filterReflectConfig(
+ @Parameters(description = "Plugin descriptor in JSON format") final File jsonPluginDescriptor,
+ @Parameters(
+ arity = "1..*",
+ description = "Classpath containing GraalVM descriptors",
+ paramLabel = "<classPathElement>")
+ final List<String> classPath)
+ throws IOException {
+ new ReflectConfigTransformer(jsonPluginDescriptor, classPath, outputDirectory).call();
+ }
+
+ private static Collection<Path> validateClassPath(final Collection<String> classPath) {
+ return classPath.stream()
+ .flatMap(classPathElement -> Arrays.stream(classPathElement.split(File.pathSeparator, -1)))
+ .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);
+ }
+ return inputPath;
+ })
+ .collect(Collectors.toList());
+ }
+
+ private static void createParentDirectories(final Path path) throws IOException {
+ final Path parent = path.getParent();
+ if (parent != null) {
+ Files.createDirectories(parent);
+ }
+ }
+
+ private static final class PluginDescriptorToJsonConverter implements Callable<@Nullable Void> {
+
+ private final ClassLoader classLoader;
+
+ private final Path outputDirectory;
+
+ PluginDescriptorToJsonConverter(final Collection<String> classPath, final File outputDirectory) {
+ this.classLoader = AccessController.doPrivileged(
+ (PrivilegedAction<ClassLoader>) () -> new URLClassLoader(validateClassPath(classPath).stream()
+ .map(p -> {
+ try {
+ return p.toUri().toURL();
+ } catch (final MalformedURLException e) {
+ throw new IllegalArgumentException(e);
+ }
+ })
+ .toArray(URL[]::new)));
+ this.outputDirectory = outputDirectory.toPath();
+ }
+
+ @Override
+ public @Nullable Void call() throws IOException {
+ final Path pluginDescriptorPath = outputDirectory.resolve(PLUGIN_DESCRIPTOR_JSON_FILE);
+ logger.info("Creating Log4j plugin descriptor file (JSON format): {}", pluginDescriptorPath);
+ createParentDirectories(pluginDescriptorPath);
+ try (final OutputStream output = Files.newOutputStream(pluginDescriptorPath);
+ 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_DESCRIPTOR_FILE))) {
+ try (final BufferedInputStream input = new BufferedInputStream(url.openStream())) {
+ pluginDescriptor.readPluginDescriptor(input);
+ }
+ }
+ // Write JSON file
+ pluginDescriptor.withBuilderHierarchy(classLoader).toJson(generator);
+ }
+ return null;
+ }
+ }
+
+ private static class JsonToPluginDescriptorConverter implements Callable<@Nullable Void> {
+
+ private final Path input;
+ private final Path outputDirectory;
+
+ JsonToPluginDescriptorConverter(final File input, final File outputDirectory) {
+ this.input = input.toPath();
+ this.outputDirectory = outputDirectory.toPath();
+ }
+
+ @Override
+ public @Nullable Void call() throws IOException {
+ final Path pluginDescriptorPath = outputDirectory.resolve(PLUGIN_DESCRIPTOR_FILE);
+ logger.info("Creating Log4j plugin descriptor file (binary format): {}", pluginDescriptorPath);
+ createParentDirectories(pluginDescriptorPath);
+ try (final OutputStream output = Files.newOutputStream(pluginDescriptorPath);
+ final InputStream inputStream = Files.newInputStream(input);
+ final JsonParser parser = jsonFactory.createParser(inputStream)) {
+ final PluginDescriptor pluginDescriptor = new PluginDescriptor();
+ // Input JSON
+ pluginDescriptor.readJson(parser);
+ // Output `Log4j2Plugins.dat` file
+ pluginDescriptor.toPluginDescriptor(output);
+ }
+ return null;
+ }
+ }
+
+ private static class ReflectConfigTransformer implements Callable<@Nullable Void> {
+
+ private final Path pluginDescriptorPath;
+ private final Collection<Path> classPath;
+ private final Path outputDirectory;
+
+ ReflectConfigTransformer(
+ final File pluginDescriptorPath, final Collection<String> classPath, final File outputDirectory) {
+ this.pluginDescriptorPath = pluginDescriptorPath.toPath();
+ this.classPath = validateClassPath(classPath);
+ this.outputDirectory = outputDirectory.toPath();
+ }
+
+ void filterReflectConfigInJar(final Path jar, final ReflectConfigFilter filter) throws IOException {
+ final URI jarFileSystemRoot;
+ try {
+ jarFileSystemRoot = new URI("jar", jar.toUri().toASCIIString(), null);
+ } catch (final URISyntaxException e) {
+ throw new IllegalArgumentException(e);
+ }
+ try (final FileSystem fileSystem = FileSystems.newFileSystem(jarFileSystemRoot, Collections.emptyMap())) {
+ final Path rootPath = fileSystem.getPath("/");
+ final Path nativeImagePath = rootPath.resolve("META-INF/native-image");
+ if (Files.isDirectory(nativeImagePath)) {
+ try (final Stream<Path> paths = Files.walk(nativeImagePath, 3)) {
+ paths.filter(p -> "reflect-config.json"
+ .equals(p.getFileName().toString()))
+ .forEach(p -> filterReflectConfig(rootPath, p, filter));
+ }
+ }
+ }
+ }
+
+ void filterReflectConfig(final Path rootPath, final Path reflectConfig, final ReflectConfigFilter filter) {
+ try {
+ final String relativePath = rootPath.relativize(reflectConfig).toString();
+ final Path outputPath = outputDirectory.resolve(relativePath);
+ logger.info("Creating GraalVM configuration file: {}", outputPath);
+ createParentDirectories(outputPath);
+ try (final OutputStream output = Files.newOutputStream(outputPath);
+ final InputStream inputStream = Files.newInputStream(reflectConfig);
+ final JsonParser parser = jsonFactory.createParser(inputStream);
+ final JsonGenerator generator = jsonFactory.createGenerator(output)) {
+ filter.filter(parser, generator);
+ }
+ } catch (final IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @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);
+ for (final Path jar : classPath) {
+ filterReflectConfigInJar(jar, filter);
+ }
+ 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..6600a1a
--- /dev/null
+++ b/log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/internal/PluginDescriptors.java
@@ -0,0 +1,464 @@
+/*
+ * 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;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+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";
+
+ private static final Logger logger = LogManager.getLogger(PluginDescriptors.class);
+
+ 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> newPluginsByClassName = new TreeMap<>();
+ pluginsByClassName.forEach((k, v) -> {
+ try {
+ newPluginsByClassName.put(k, v.withBuilderHierarchy(classLoader));
+ } catch (final PluginNotFoundException e) {
+ // No need to bother the user with the full cause
+ // Since we use Simple Logger, we only print the NoClassDefFound message.
+ logger.warn(
+ "Skipping plugin {} because it can not be loaded: {}",
+ e.getMessage(),
+ e.getCause().toString());
+ }
+ });
+ return new Namespace(name, Collections.unmodifiableMap(newPluginsByClassName));
+ }
+
+ 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 Collections.unmodifiableSet(pluginNames);
+ }
+
+ public String getClassName() {
+ return className;
+ }
+
+ public List<String> getBuilderHierarchy() {
+ return Collections.unmodifiableList(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));
+ 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 List<String> findBuilderClassHierarchy(final ClassLoader classLoader) {
+ 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:
+ // Continue the search until apache/logging-log4j2#3126 is fixed.
+ break;
+ case PLUGIN_BUILDER_FACTORY:
+ return computeClassHierarchy(findBuilderClass(method.getGenericReturnType()));
+ }
+ }
+ }
+ return Collections.emptyList();
+ } catch (final ClassNotFoundException | LinkageError e) {
+ throw new PluginNotFoundException(pluginNames, 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);
+ }
+ }
+
+ private static final class PluginNotFoundException extends RuntimeException {
+
+ private PluginNotFoundException(final Set<String> pluginNames, final Throwable cause) {
+ super(pluginNames.toString(), cause);
+ }
+ }
+}
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..3999438
--- /dev/null
+++ b/log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/internal/ReflectConfigFilter.java
@@ -0,0 +1,60 @@
+/*
+ * 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.HashSet;
+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 = new HashSet<>(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")
+ || name.startsWith("org.apache.logging.log4j.core.config.plugins.validation.validators")
+ || 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-converter-plugin-descriptor/src/main/shaded-resources/log4j2.component.properties b/log4j-converter-plugin-descriptor/src/main/shaded-resources/log4j2.component.properties
new file mode 100644
index 0000000..a86ef7a
--- /dev/null
+++ b/log4j-converter-plugin-descriptor/src/main/shaded-resources/log4j2.component.properties
@@ -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.
+#
+##
+# Enable Simple Logger
+#
+log4j.provider = org.apache.logging.log4j.simple.internal.SimpleProvider
diff --git a/log4j-converter-plugin-descriptor/src/main/shaded-resources/log4j2.simplelog.properties b/log4j-converter-plugin-descriptor/src/main/shaded-resources/log4j2.simplelog.properties
new file mode 100644
index 0000000..c43843c
--- /dev/null
+++ b/log4j-converter-plugin-descriptor/src/main/shaded-resources/log4j2.simplelog.properties
@@ -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.
+#
+##
+# Configures Simple Logger for the shaded package
+#
+log4j2.simplelogLevel = INFO
diff --git a/log4j-transform-parent/pom.xml b/log4j-transform-parent/pom.xml
index 31e48a0..be09e8e 100644
--- a/log4j-transform-parent/pom.xml
+++ b/log4j-transform-parent/pom.xml
@@ -40,13 +40,13 @@
<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>
<!-- plugin versions -->
<jacoco-maven-plugin.version>0.8.12</jacoco-maven-plugin.version>
<maven-invoker-plugin.version>3.6.1</maven-invoker-plugin.version>
- <maven-shade-plugin.version>3.5.0</maven-shade-plugin.version>
<surefire.version>3.0.0-M7</surefire.version>
</properties>
@@ -121,6 +121,12 @@
</dependency>
<dependency>
+ <groupId>info.picocli</groupId>
+ <artifactId>picocli</artifactId>
+ <version>${picocli.version}</version>
+ </dependency>
+
+ <dependency>
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-utils</artifactId>
<version>${plexus-utils.version}</version>
@@ -151,17 +157,12 @@
<version>${maven-invoker-plugin.version}</version>
</plugin>
- <plugin>
- <groupId>org.apache.maven</groupId>
- <artifactId>maven-shade-plugin</artifactId>
- <version>${maven-shade-plugin.version}</version>
- </plugin>
-
</plugins>
</pluginManagement>
</build>
<profiles>
+
<profile>
<id>java8-tests</id>
<properties>
@@ -181,6 +182,75 @@
</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>
+ <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/DEPENDENCIES</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>
+ </profile>
+
</profiles>
</project>
diff --git a/pom.xml b/pom.xml
index c9887ea..e220926 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>
@@ -134,6 +135,12 @@
<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>
<version>${project.version}</version>
</dependency>
diff --git a/src/site/antora/modules/ROOT/pages/cli.adoc b/src/site/antora/modules/ROOT/pages/cli.adoc
index 43bb43b..5ae4da8 100644
--- a/src/site/antora/modules/ROOT/pages/cli.adoc
+++ b/src/site/antora/modules/ROOT/pages/cli.adoc
@@ -73,4 +73,189 @@
[source,subs="+attributes"]
----
java -jar log4j-codegen-{project-version}.jar extendedLogger DIAG=350 NOTICE=450 VERBOSE=550
-----
\ No newline at end of file
+----
+
+[#log4j-converter-plugin-descriptor]
+== `log4j-converter-plugin-descriptor`
+
+The `log4j-converter-plugin-descriptor` tool helps you to create custom
+https://logging.apache.org/log4j/2.x/manual/plugins.html#plugin-registry[plugin descriptors]
+and align their content with the
+https://www.graalvm.org/latest/reference-manual/native-image/metadata/[GraalVM reachability metadata].
+This can be used to create smaller GraalVM native images by removing the parts of Log4j Core that are not used by the application.
+
+[NOTE]
+====
+Custom plugin descriptors are not required for applications running in the JVM.
+If you are
+https://logging.apache.org/log4j/2.x/faq.html#shading[shading/shadowing your application],
+and you need to merge multiple plugin descriptors, use the
+xref:log4j-transform-maven-shade-plugin-extensions.adoc#log4j-plugin-cache-transformer[Log4j Plugin Descriptor Transformer]
+instead.
+====
+
+To create a custom plugin descriptor and reachability metadata descriptor, you need to:
+
+. Extract the information contained in the `Log4j2Plugins.dat` descriptors in your runtime classpath.
+See <<log4j-converter-plugin-descriptor-toJson>> on how to do it.
+. Select the plugins that you want in your GraalVM application.
+See <<log4j-converter-plugin-descriptor-select>> for some tips on how to do it.
+. Convert your list of plugins back into the `Log4j2Plugins.dat` format.
+See <<log4j-converter-plugin-descriptor-fromJson>> for more information.
+. Create a custom `reflect-config.json` using the reduced list of Log4j plugins.
+See <<log4j-converter-plugin-descriptor-filterReflectConfig>> for more details.
+
+[#log4j-converter-plugin-descriptor-toJson]
+=== Converting from `Log4j2Plugins.dat` to `Log4j2Plugins.json`
+
+To convert all the `Log4j2Plugins.dat` files on your application's classpath run:
+
+[source,subs="+attributes"]
+----
+java -jar log4j-converter-plugin-descriptor-{project-version}.jar \
+ toJson [-o=<outputDirectory>] <classPathElement>...
+----
+
+where:
+
+`<outputDirectory>`::
+The directory, where the command's output will be saved.
+Defaults to the current working directory.
+
+`<classPathElement>`::
+A list of file paths to the runtime dependencies of your application, separated by either spaces or your system path separator (`:` for UNIX and `;` for Windows).
+
+The command will generate a `Log4j2Plugins.json` file in the output directory.
+
+[#log4j-converter-plugin-descriptor-select]
+=== Selecting plugins
+
+The `Log4j2Plugins.json` file contains all the
+https://logging.apache.org/log4j/2.x/manual/plugins.html#declare-plugin[Log4j Plugins]
+contained on your classpath and grouped by category/namespace.
+A functional Log4j Core installation needs these categories:
+
+`configurationfactory`::
++
+Unless you have a
+https://logging.apache.org/log4j/2.x/manual/customconfig.html#ConfigurationFactory[custom `ConfigurationFactory`]
+you need to include at least the configuration factory for your configuration format.
+
+`core`::
+This category contains all the plugins that can be used in a configuration file.
+You can browse the
+https://logging.apache.org/log4j/2.x/plugin-reference.html#org-apache-logging-log4j_log4j-core_org-apache-logging-log4j-core-config-Configuration[plugin reference]
+to choose those that you need.
+A minimal Log4j Core installation will certainly need:
+--
+* The
+https://logging.apache.org/log4j/2.x/plugin-reference.html#org-apache-logging-log4j_log4j-core_org-apache-logging-log4j-core-config-AppendersPlugin[`Appenders`]
+and
+https://logging.apache.org/log4j/2.x/plugin-reference.html#org-apache-logging-log4j_log4j-core_org-apache-logging-log4j-core-config-LoggersPlugin[`Loggers`]
+plugins.
+* Either the
+https://logging.apache.org/log4j/2.x/plugin-reference.html#org-apache-logging-log4j_log4j-core_org-apache-logging-log4j-core-config-LoggerConfig-RootLogger[`Root`]
+and
+https://logging.apache.org/log4j/2.x/plugin-reference.html#org-apache-logging-log4j_log4j-core_org-apache-logging-log4j-core-config-LoggerConfig[`Logger`]
+plugins or the
+https://logging.apache.org/log4j/2.x/plugin-reference.html#org-apache-logging-log4j_log4j-core_org-apache-logging-log4j-core-async-AsyncLoggerConfig-RootLogger[`AsyncRoot`]
+and
+https://logging.apache.org/log4j/2.x/plugin-reference.html#org-apache-logging-log4j_log4j-core_org-apache-logging-log4j-core-async-AsyncLoggerConfig[`AsyncLogger`] plugins.
+* At least one
+https://logging.apache.org/log4j/2.x/plugin-reference.html#org-apache-logging-log4j_log4j-core_org-apache-logging-log4j-core-Appender[appender plugin].
+See
+https://logging.apache.org/log4j/2.x/manual/appenders.html[Appenders]
+for more information on appenders.
+* At least one
+https://logging.apache.org/log4j/2.x/plugin-reference.html#org-apache-logging-log4j_log4j-core_org-apache-logging-log4j-core-Layout[layout plugin].
+See
+https://logging.apache.org/log4j/2.x/manual/layouts.html[Layouts]
+for more information on layouts.
+--
+If you plan to define properties for
+https://logging.apache.org/log4j/2.x/manual/configuration.html#property-substitution[property substitution]
+in your configuration file, consider adding the
+https://logging.apache.org/log4j/2.x/plugin-reference.html#org-apache-logging-log4j_log4j-core_org-apache-logging-log4j-core-config-PropertiesPlugin[`Properties`]
+and
+https://logging.apache.org/log4j/2.x/plugin-reference.html#org-apache-logging-log4j_log4j-core_org-apache-logging-log4j-core-config-Property[`Property`]
+plugins.
+
+`converter`::
+If you plan to use
+https://logging.apache.org/log4j/2.x/manual/pattern-layout.html[Pattern Layout]
+you need to add some
+https://logging.apache.org/log4j/2.x/plugin-reference.html#org-apache-logging-log4j_log4j-core_org-apache-logging-log4j-core-pattern-PatternConverter[pattern converter plugins].
+
+`jsontemplateresolverfactory`::
+To use
+https://logging.apache.org/log4j/2.x/manual/json-template-layout.html[JSON Template Layout]
+you need to add some
+https://logging.apache.org/log4j/2.x/plugin-reference.html#org-apache-logging-log4j_log4j-layout-template-json_org-apache-logging-log4j-layout-template-json-resolver-TemplateResolverFactory[template resolver factories].
+
+`lookup`::
+The `lookup` category contains
+https://logging.apache.org/log4j/2.x/manual/lookups.html[lookups]
+that can be used to retrieve configuration values from external sources.
+See also
+https://logging.apache.org/log4j/2.x/plugin-reference.html#org-apache-logging-log4j_log4j-core_org-apache-logging-log4j-core-lookup-StrLookup[lookup plugins]
+for a list of options.
+
+`typeconverter`::
+This category provides basic conversion capabilities.
+Unless you know what you are doing, keep all the plugins.
+
+[#log4j-converter-plugin-descriptor-fromJson]
+=== Creating a custom `Log4j2Plugins.dat` file
+
+Once you have chosen the plugins for your Log4j Core custom image, you need to convert the modified `Log4j2Plugins.json` file back to its original format.
+To do that run:
+
+[source,subs="+attributes"]
+----
+java -jar log4j-converter-plugin-descriptor-{project-version}.jar \
+ fromJson [-o=<outputDirectory>] <jsonPluginDescriptor>
+----
+
+where:
+
+`<outputDirectory>`::
+The directory, where the command's output will be saved.
+This parameter should point at the root of your application's classpath (e.g., the `src/main/resources`) folder.
+Defaults to the current working directory.
+
+`<jsonPluginDescriptor>`::
+The path to the `Log4j2Plugins.json` file.
+
+The command will generate a `Log4j2Plugins.dat` file in the `org/apache/logging/log4j/core/config/plugins` subfolder of the output directory.
+
+[#log4j-converter-plugin-descriptor-filterReflectConfig]
+=== Creating a custom `reflect-config.json` file
+
+The same `Log4j2Plugins.json` file can be used to trim the
+https://www.graalvm.org/latest/reference-manual/native-image/metadata/[GraalVM reachability metadata]
+embedded in Log4j `2.25.0` and later, so that they contain only the classes required by the selected plugins.
+To extract all the `reflect-config.json` files from your runtime classpath and remove the unnecessary classes run:
+
+[source,subs="+attributes"]
+----
+java -jar log4j-converter-plugin-descriptor-{project-version}.jar \
+ filterReflectConfig [-o=<outputDirectory>] <jsonPluginDescriptor> <classPathElement>...
+----
+
+where:
+
+`<outputDirectory>`::
+The directory, where the command's output will be saved.
+This parameter should point at the root of your application's classpath (e.g., the `src/main/resources`) folder.
+Defaults to the current working directory.
+
+`<jsonPluginDescriptor>`::
+The path to the `Log4j2Plugins.json` file.
+
+`<classPathElement>`::
+A list of file paths to the runtime dependencies of your application, separated by either spaces or your system path separator (`:` for UNIX and `;` for Windows).
+
+The command will filter and output each `reflect-config.json` in its original path under the `META-INF/native-image` subfolder of the output directory.
+
+[#log4j-converter-plugin-descriptor-example]
+=== Examples
\ No newline at end of file
diff --git a/src/site/antora/modules/ROOT/pages/log4j-transform-maven-shade-plugin-extensions.adoc b/src/site/antora/modules/ROOT/pages/log4j-transform-maven-shade-plugin-extensions.adoc
index 71ce292..5ed11ca 100644
--- a/src/site/antora/modules/ROOT/pages/log4j-transform-maven-shade-plugin-extensions.adoc
+++ b/src/site/antora/modules/ROOT/pages/log4j-transform-maven-shade-plugin-extensions.adoc
@@ -20,7 +20,7 @@
This project contains a collection of https://maven.apache.org/plugins/maven-shade-plugin/examples/resource-transformers.html[resource transformer]s for the Apache Maven Shade Plugin that allows you to use additional Log4j 2.x Core component modules.
[#log4j-plugin-cache-transformer]
-== Log4j Plugin Cache Transformer
+== Log4j Plugin Descriptor Transformer
A
https://maven.apache.org/plugins/maven-shade-plugin/examples/resource-transformers.html[resource transformer]