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]