You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by ma...@apache.org on 2025/08/15 17:36:38 UTC
(logging-log4j2) branch feature/3.x/graalvm-reachability created (now 0d6fbafc30)
This is an automated email from the ASF dual-hosted git repository.
mattsicker pushed a change to branch feature/3.x/graalvm-reachability
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
at 0d6fbafc30 Port GraalVM reachability to 3.x
This branch includes the following new commits:
new d4829f94c1 Extract class PluginIndex
new aaeaf12745 Prepare PluginProcessor for introduction of GraalVmProcessor
new 0d6fbafc30 Port GraalVM reachability to 3.x
The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(logging-log4j2) 02/03: Prepare PluginProcessor for introduction of GraalVmProcessor
Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
mattsicker pushed a commit to branch feature/3.x/graalvm-reachability
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
commit aaeaf127453cd8f7f5408a477a49d4d595aa6ac7
Author: Matt Sicker <ms...@apple.com>
AuthorDate: Thu Aug 14 11:41:32 2025 -0500
Prepare PluginProcessor for introduction of GraalVmProcessor
This refactors and updates PluginProcessor to function more similarly to GraalVmProcessor. This re-uses the recently added PluginIndex class for tracking processed plugins before dumping this data in the final annotation processing round.
---
.../log4j/plugin/processor/PluginProcessor.java | 263 ++++++++++-----------
.../plugin/processor/PluginProcessorTest.java | 25 +-
2 files changed, 134 insertions(+), 154 deletions(-)
diff --git a/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/PluginProcessor.java b/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/PluginProcessor.java
index 8293bac51b..ff41a4fdc9 100644
--- a/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/PluginProcessor.java
+++ b/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/PluginProcessor.java
@@ -25,26 +25,24 @@ import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
+import java.util.Arrays;
+import java.util.HashSet;
import java.util.List;
import java.util.Locale;
-import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
+import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationValue;
-import javax.lang.model.element.Element;
-import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
-import javax.lang.model.util.SimpleElementVisitor8;
+import javax.lang.model.util.ElementFilter;
+import javax.lang.model.util.Elements;
import javax.tools.Diagnostic.Kind;
import javax.tools.FileObject;
import javax.tools.JavaFileObject;
@@ -52,10 +50,10 @@ import javax.tools.StandardLocation;
import org.apache.logging.log4j.LoggingException;
import org.apache.logging.log4j.plugins.Configurable;
import org.apache.logging.log4j.plugins.Namespace;
-import org.apache.logging.log4j.plugins.Node;
import org.apache.logging.log4j.plugins.Plugin;
import org.apache.logging.log4j.plugins.PluginAliases;
import org.apache.logging.log4j.plugins.model.PluginEntry;
+import org.apache.logging.log4j.plugins.model.PluginIndex;
import org.apache.logging.log4j.util.Strings;
import org.jspecify.annotations.NullMarked;
@@ -74,7 +72,7 @@ import org.jspecify.annotations.NullMarked;
* </p>
*/
@NullMarked
-@SupportedAnnotationTypes({"org.apache.logging.log4j.plugins.*", "org.apache.logging.log4j.core.config.plugins.*"})
+@SupportedAnnotationTypes("org.apache.logging.log4j.plugins.Plugin")
@ServiceProvider(value = Processor.class, resolution = Resolution.OPTIONAL)
public class PluginProcessor extends AbstractProcessor {
@@ -98,7 +96,9 @@ public class PluginProcessor extends AbstractProcessor {
"META-INF/services/org.apache.logging.log4j.plugins.model.PluginService";
private boolean enableBndAnnotations;
- private String packageName = "";
+ private CharSequence packageName = "";
+ private final PluginIndex pluginIndex = new PluginIndex();
+ private final Set<TypeElement> processedElements = new HashSet<>();
public PluginProcessor() {}
@@ -112,89 +112,124 @@ public class PluginProcessor extends AbstractProcessor {
return SourceVersion.latest();
}
+ @Override
+ public synchronized void init(ProcessingEnvironment processingEnv) {
+ super.init(processingEnv);
+ handleOptions();
+ }
+
@Override
public boolean process(final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnv) {
- handleOptions(processingEnv.getOptions());
- final Messager messager = processingEnv.getMessager();
- messager.printMessage(Kind.NOTE, "Processing Log4j annotations");
- try {
- final Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Plugin.class);
- if (elements.isEmpty()) {
- messager.printMessage(Kind.NOTE, "No elements to process");
- return true;
+ // Process the elements for this round
+ if (!annotations.isEmpty()) {
+ processPluginAnnotatedClasses(ElementFilter.typesIn(roundEnv.getElementsAnnotatedWith(Plugin.class)));
+ }
+ // Write the generated code
+ if (roundEnv.processingOver() && !pluginIndex.isEmpty()) {
+ try {
+ final Messager messager = processingEnv.getMessager();
+ messager.printMessage(Kind.NOTE, "Writing Log4j plugin metadata using base package " + packageName);
+ writeClassFile();
+ writeServiceFile();
+ messager.printMessage(Kind.NOTE, "Log4j annotations processed");
+ } catch (final Exception e) {
+ handleUnexpectedError(e);
}
- messager.printMessage(Kind.NOTE, "Retrieved " + elements.size() + " Plugin elements");
- final List<PluginEntry> list = new ArrayList<>();
- packageName = collectPlugins(packageName, elements, list);
- messager.printMessage(Kind.NOTE, "Writing plugin metadata using base package " + packageName);
- Collections.sort(list);
- writeClassFile(packageName, list);
- writeServiceFile(packageName);
- messager.printMessage(Kind.NOTE, "Annotations processed");
- } catch (final Exception ex) {
- var writer = new StringWriter();
- ex.printStackTrace(new PrintWriter(writer));
- error(writer.toString());
}
+ // Do not claim the annotations to allow other annotation processors to run
return false;
}
- private void error(final CharSequence message) {
- processingEnv.getMessager().printMessage(Kind.ERROR, message);
+ private void processPluginAnnotatedClasses(Set<TypeElement> pluginClasses) {
+ final boolean calculatePackageName = packageName.isEmpty();
+ final Elements elements = processingEnv.getElementUtils();
+ final Messager messager = processingEnv.getMessager();
+ for (var pluginClass : pluginClasses) {
+ final String name = getPluginName(pluginClass);
+ final String namespace = getNamespace(pluginClass);
+ final String className = elements.getBinaryName(pluginClass).toString();
+ var builder =
+ PluginEntry.builder().setName(name).setNamespace(namespace).setClassName(className);
+ processConfigurableAnnotation(pluginClass, builder);
+ var entry = builder.get();
+ messager.printMessage(Kind.NOTE, "Parsed Log4j plugin " + entry, pluginClass);
+ if (!pluginIndex.add(entry)) {
+ messager.printMessage(Kind.WARNING, "Duplicate Log4j plugin parsed " + entry, pluginClass);
+ }
+ pluginIndex.addAll(createPluginAliases(pluginClass, builder));
+ if (calculatePackageName) {
+ packageName = calculatePackageName(elements, pluginClass, packageName);
+ }
+ processedElements.add(pluginClass);
+ }
}
- private String collectPlugins(
- String packageName, final Iterable<? extends Element> elements, final List<PluginEntry> list) {
- final boolean calculatePackage = packageName.isEmpty();
- final var pluginVisitor = new PluginElementVisitor();
- final var pluginAliasesVisitor = new PluginAliasesElementVisitor();
- for (final Element element : elements) {
- // The elements must be annotated with `Plugin`
- Plugin plugin = element.getAnnotation(Plugin.class);
- final var entry = element.accept(pluginVisitor, plugin);
- list.add(entry);
- if (calculatePackage) {
- packageName = calculatePackage(element, packageName);
- }
- list.addAll(element.accept(pluginAliasesVisitor, plugin));
+ private static void processConfigurableAnnotation(TypeElement pluginClass, PluginEntry.Builder builder) {
+ var configurable = pluginClass.getAnnotation(Configurable.class);
+ if (configurable != null) {
+ var elementType = configurable.elementType();
+ builder.setElementType(elementType.isEmpty() ? builder.getName() : elementType)
+ .setDeferChildren(configurable.deferChildren())
+ .setPrintable(configurable.printObject());
}
- return packageName;
}
- private String calculatePackage(Element element, String packageName) {
- final Name name = processingEnv.getElementUtils().getPackageOf(element).getQualifiedName();
- if (name.isEmpty()) {
- return "";
+ private static List<PluginEntry> createPluginAliases(TypeElement pluginClass, PluginEntry.Builder builder) {
+ return Optional.ofNullable(pluginClass.getAnnotation(PluginAliases.class)).map(PluginAliases::value).stream()
+ .flatMap(Arrays::stream)
+ .map(alias -> alias.toLowerCase(Locale.ROOT))
+ .map(key -> builder.setKey(key).get())
+ .toList();
+ }
+
+ private void handleUnexpectedError(final Exception e) {
+ var writer = new StringWriter();
+ e.printStackTrace(new PrintWriter(writer));
+ processingEnv
+ .getMessager()
+ .printMessage(Kind.ERROR, "Unexpected error processing Log4j annotations: " + writer);
+ }
+
+ private static CharSequence calculatePackageName(
+ Elements elements, TypeElement typeElement, CharSequence packageName) {
+ var qualifiedName = elements.getPackageOf(typeElement).getQualifiedName();
+ if (qualifiedName.isEmpty()) {
+ return packageName;
}
- final String pkgName = name.toString();
if (packageName.isEmpty()) {
- return pkgName;
+ return qualifiedName;
}
- if (pkgName.length() == packageName.length()) {
+ int packageLength = packageName.length();
+ int qualifiedLength = qualifiedName.length();
+ if (packageLength == qualifiedLength) {
return packageName;
}
- if (pkgName.length() < packageName.length() && packageName.startsWith(pkgName)) {
- return pkgName;
+ if (qualifiedLength < packageLength
+ && qualifiedName.contentEquals(packageName.subSequence(0, qualifiedLength))) {
+ return qualifiedName;
}
-
- return commonPrefix(pkgName, packageName);
+ return commonPrefix(qualifiedName, packageName);
}
- private void writeServiceFile(final String pkgName) throws IOException {
+ private void writeServiceFile() throws IOException {
final FileObject fileObject = processingEnv
.getFiler()
- .createResource(StandardLocation.CLASS_OUTPUT, Strings.EMPTY, SERVICE_FILE_NAME);
+ .createResource(
+ StandardLocation.CLASS_OUTPUT,
+ Strings.EMPTY,
+ SERVICE_FILE_NAME,
+ processedElements.toArray(TypeElement[]::new));
try (final PrintWriter writer =
new PrintWriter(new BufferedWriter(new OutputStreamWriter(fileObject.openOutputStream(), UTF_8)))) {
writer.println("# Generated by " + PluginProcessor.class.getName());
- writer.println(createFqcn(pkgName));
+ writer.println(createFqcn(packageName));
}
}
- private void writeClassFile(final String pkg, final List<PluginEntry> list) {
- final String fqcn = createFqcn(pkg);
+ private void writeClassFile() {
+ final String fqcn = createFqcn(packageName);
try (final PrintWriter writer = createSourceFile(fqcn)) {
- writer.println("package " + pkg + ".plugins;");
+ writer.println("package " + packageName + ".plugins;");
writer.println("");
if (enableBndAnnotations) {
writer.println("import aQute.bnd.annotation.Resolution;");
@@ -209,9 +244,9 @@ public class PluginProcessor extends AbstractProcessor {
writer.println("public class Log4jPlugins extends PluginService {");
writer.println("");
writer.println(" private static final PluginEntry[] ENTRIES = new PluginEntry[] {");
- final int max = list.size() - 1;
- for (int i = 0; i < list.size(); ++i) {
- final PluginEntry entry = list.get(i);
+ final int max = pluginIndex.size() - 1;
+ int current = 0;
+ for (final PluginEntry entry : pluginIndex) {
writer.println(" PluginEntry.builder()");
writer.println(String.format(" .setKey(\"%s\")", entry.key()));
writer.println(String.format(" .setClassName(\"%s\")", entry.className()));
@@ -227,7 +262,8 @@ public class PluginProcessor extends AbstractProcessor {
if (entry.deferChildren()) {
writer.println(" .setDeferChildren(true)");
}
- writer.println(" .get()" + (i < max ? "," : Strings.EMPTY));
+ writer.println(" .get()" + (current < max ? "," : Strings.EMPTY));
+ current++;
}
writer.println(" };");
writer.println(" @Override");
@@ -238,17 +274,25 @@ public class PluginProcessor extends AbstractProcessor {
private PrintWriter createSourceFile(final String fqcn) {
try {
- final JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(fqcn);
+ final JavaFileObject sourceFile =
+ processingEnv.getFiler().createSourceFile(fqcn, processedElements.toArray(TypeElement[]::new));
return new PrintWriter(sourceFile.openWriter());
} catch (IOException e) {
throw new LoggingException("Unable to create Plugin Service Class " + fqcn, e);
}
}
- private String createFqcn(String packageName) {
+ private String createFqcn(CharSequence packageName) {
return packageName + ".plugins.Log4jPlugins";
}
+ private static String getPluginName(TypeElement pluginClass) {
+ return Optional.ofNullable(pluginClass.getAnnotation(Plugin.class))
+ .map(Plugin::value)
+ .filter(s -> !s.isEmpty())
+ .orElseGet(() -> pluginClass.getSimpleName().toString());
+ }
+
private static String getNamespace(final TypeElement e) {
return Optional.ofNullable(e.getAnnotation(Namespace.class))
.map(Namespace::value)
@@ -267,52 +311,18 @@ public class PluginProcessor extends AbstractProcessor {
.orElse(Plugin.EMPTY));
}
- private static PluginEntry configureNamespace(final TypeElement e, final PluginEntry.Builder builder) {
- final Configurable configurable = e.getAnnotation(Configurable.class);
- if (configurable != null) {
- builder.setNamespace(Node.CORE_NAMESPACE)
- .setElementType(
- configurable.elementType().isEmpty() ? builder.getName() : configurable.elementType())
- .setDeferChildren(configurable.deferChildren())
- .setPrintable(configurable.printObject());
- } else {
- builder.setNamespace(getNamespace(e));
- }
- return builder.get();
- }
-
- /**
- * ElementVisitor to scan the Plugin annotation.
- */
- private final class PluginElementVisitor extends SimpleElementVisitor8<PluginEntry, Plugin> {
- @Override
- public PluginEntry visitType(final TypeElement e, final Plugin plugin) {
- Objects.requireNonNull(plugin, "Plugin annotation is null.");
- String name = plugin.value();
- if (name.isEmpty()) {
- name = e.getSimpleName().toString();
- }
- final PluginEntry.Builder builder = PluginEntry.builder()
- .setKey(name.toLowerCase(Locale.ROOT))
- .setName(name)
- .setClassName(
- processingEnv.getElementUtils().getBinaryName(e).toString());
- return configureNamespace(e, builder);
- }
- }
-
- private String commonPrefix(final String str1, final String str2) {
+ private static CharSequence commonPrefix(final CharSequence str1, final CharSequence str2) {
final int minLength = Math.min(str1.length(), str2.length());
for (int i = 0; i < minLength; i++) {
if (str1.charAt(i) != str2.charAt(i)) {
if (i > 1 && str1.charAt(i - 1) == '.') {
- return str1.substring(0, i - 1);
+ return str1.subSequence(0, i - 1);
} else {
- return str1.substring(0, i);
+ return str1.subSequence(0, i);
}
}
}
- return str1.substring(0, minLength);
+ return str1.subSequence(0, minLength);
}
private boolean isServiceConsumerClassPresent() {
@@ -320,7 +330,8 @@ public class PluginProcessor extends AbstractProcessor {
return processingEnv.getElementUtils().getTypeElement("aQute.bnd.annotation.spi.ServiceConsumer") != null;
}
- private void handleOptions(Map<String, String> options) {
+ private void handleOptions() {
+ var options = processingEnv.getOptions();
packageName = options.getOrDefault(PLUGIN_PACKAGE, "");
String enableBndAnnotationsOption = options.get(ENABLE_BND_ANNOTATIONS);
if (enableBndAnnotationsOption != null) {
@@ -329,38 +340,4 @@ public class PluginProcessor extends AbstractProcessor {
this.enableBndAnnotations = isServiceConsumerClassPresent();
}
}
-
- /**
- * ElementVisitor to scan the PluginAliases annotation.
- */
- private final class PluginAliasesElementVisitor extends SimpleElementVisitor8<Collection<PluginEntry>, Plugin> {
-
- private PluginAliasesElementVisitor() {
- super(List.of());
- }
-
- @Override
- public Collection<PluginEntry> visitType(final TypeElement e, final Plugin plugin) {
- final PluginAliases aliases = e.getAnnotation(PluginAliases.class);
- if (aliases == null) {
- return DEFAULT_VALUE;
- }
- String name = plugin.value();
- if (name.isEmpty()) {
- name = e.getSimpleName().toString();
- }
- final PluginEntry.Builder builder = PluginEntry.builder()
- .setName(name)
- .setClassName(
- processingEnv.getElementUtils().getBinaryName(e).toString());
- configureNamespace(e, builder);
- final Collection<PluginEntry> entries = new ArrayList<>(aliases.value().length);
- for (final String alias : aliases.value()) {
- final PluginEntry entry =
- builder.setKey(alias.toLowerCase(Locale.ROOT)).get();
- entries.add(entry);
- }
- return entries;
- }
- }
}
diff --git a/log4j-plugin-processor/src/test/java/org/apache/logging/log4j/plugin/processor/PluginProcessorTest.java b/log4j-plugin-processor/src/test/java/org/apache/logging/log4j/plugin/processor/PluginProcessorTest.java
index b6c2e7033e..d3d1f88ece 100644
--- a/log4j-plugin-processor/src/test/java/org/apache/logging/log4j/plugin/processor/PluginProcessorTest.java
+++ b/log4j-plugin-processor/src/test/java/org/apache/logging/log4j/plugin/processor/PluginProcessorTest.java
@@ -16,18 +16,18 @@
*/
package org.apache.logging.log4j.plugin.processor;
-import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Set;
@@ -39,7 +39,7 @@ import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.StandardLocation;
import javax.tools.ToolProvider;
-import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.file.PathUtils;
import org.apache.logging.log4j.plugins.model.PluginEntry;
import org.apache.logging.log4j.plugins.model.PluginNamespace;
import org.apache.logging.log4j.plugins.model.PluginService;
@@ -86,7 +86,8 @@ class PluginProcessorTest {
try {
// Instantiate the tooling
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
- StandardJavaFileManager fileManager = compiler.getStandardFileManager(collector, Locale.ROOT, UTF_8);
+ StandardJavaFileManager fileManager =
+ compiler.getStandardFileManager(collector, Locale.ROOT, StandardCharsets.UTF_8);
// Populate sources
Iterable<? extends JavaFileObject> sources = fileManager.getJavaFileObjects(fakePluginPath);
@@ -97,21 +98,24 @@ class PluginProcessorTest {
// Compile the sources
final JavaCompiler.CompilationTask task =
- compiler.getTask(null, fileManager, collector, Arrays.asList(options), null, sources);
+ compiler.getTask(null, fileManager, collector, List.of(options), null, sources);
task.setProcessors(List.of(new PluginProcessor()));
- task.call();
+ Boolean result = task.call();
// Verify successful compilation
- List<Diagnostic<? extends JavaFileObject>> diagnostics = collector.getDiagnostics();
- assertThat(diagnostics).isEmpty();
+ assertEquals(true, result);
+ assertThat(collector.getDiagnostics()).isEmpty();
// Find the PluginService class
Path pluginServicePath = outputDir.resolve(fqcn.replaceAll("\\.", "/") + ".class");
assertThat(pluginServicePath).exists();
Class<?> pluginServiceClass = classLoader.defineClass(fqcn, pluginServicePath);
- return (PluginService) pluginServiceClass.getConstructor().newInstance();
+ return pluginServiceClass
+ .asSubclass(PluginService.class)
+ .getConstructor()
+ .newInstance();
} finally {
- FileUtils.deleteDirectory(outputDir.toFile());
+ PathUtils.deleteDirectory(outputDir);
}
}
@@ -227,7 +231,6 @@ class PluginProcessorTest {
public void report(Diagnostic<? extends JavaFileObject> diagnostic) {
switch (diagnostic.getKind()) {
case ERROR:
- case WARNING:
case MANDATORY_WARNING:
diagnostics.add(diagnostic);
break;
(logging-log4j2) 03/03: Port GraalVM reachability to 3.x
Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
mattsicker pushed a commit to branch feature/3.x/graalvm-reachability
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
commit 0d6fbafc30ab5d4ae882dd7a52e2c396e0b07035
Author: Matt Sicker <ms...@apple.com>
AuthorDate: Fri Aug 15 12:35:45 2025 -0500
Port GraalVM reachability to 3.x
This ports the GraalVmProcessor introduced in a14f0ad57ed380b19ddd715df6f3651acea6f8eb (released in 2.25.0) to the plugin model of 3.x.
---
log4j-parent/pom.xml | 3 +
.../log4j/plugin/processor/GraalVmProcessor.java | 355 +++++++++++++++++++++
.../plugin/processor/internal/Annotations.java | 149 +++++++++
.../processor/internal/ReachabilityMetadata.java | 296 +++++++++++++++++
.../plugin/processor/GraalVmProcessorTest.java | 70 ++++
.../example/AbstractPluginWithGenericBuilder.java | 58 ++++
.../test/resources/example/ConfigurablePlugin.java | 63 ++++
.../test/resources/example/ConfigurableRecord.java | 33 ++
.../PluginWithGenericSubclassFoo1Builder.java | 66 ++++
.../test/resources/example/ValidatingPlugin.java | 64 ++++
.../ValidatingPluginWithGenericBuilder.java | 70 ++++
.../example/ValidatingPluginWithTypedBuilder.java | 65 ++++
.../test/resources/expected-reflect-config.json | 15 +
13 files changed, 1307 insertions(+)
diff --git a/log4j-parent/pom.xml b/log4j-parent/pom.xml
index 1f1b6a7575..e2f940675c 100644
--- a/log4j-parent/pom.xml
+++ b/log4j-parent/pom.xml
@@ -866,6 +866,9 @@
<arg>-Alog4j.docgen.version=${project.version}</arg>
<arg>-Alog4j.docgen.description=${project.description}</arg>
<arg>-Alog4j.docgen.typeFilter.excludePattern=${log4j.docgen.typeFilter.excludePattern}</arg>
+ <!-- Provide arguments for the GraalVM processor -->
+ <arg>-Alog4j.graalvm.groupId=${project.groupId}</arg>
+ <arg>-Alog4j.graalvm.artifactId=${project.artifactId}</arg>
</compilerArgs>
</configuration>
diff --git a/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/GraalVmProcessor.java b/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/GraalVmProcessor.java
new file mode 100644
index 0000000000..0f67a29f89
--- /dev/null
+++ b/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/GraalVmProcessor.java
@@ -0,0 +1,355 @@
+/*
+ * 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.plugin.processor;
+
+import aQute.bnd.annotation.Resolution;
+import aQute.bnd.annotation.spi.ServiceProvider;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.processing.AbstractProcessor;
+import javax.annotation.processing.Messager;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.annotation.processing.Processor;
+import javax.annotation.processing.RoundEnvironment;
+import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.annotation.processing.SupportedOptions;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.PackageElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.ArrayType;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.util.SimpleElementVisitor8;
+import javax.lang.model.util.SimpleTypeVisitor8;
+import javax.tools.Diagnostic;
+import javax.tools.StandardLocation;
+import org.apache.logging.log4j.plugin.processor.internal.Annotations;
+import org.apache.logging.log4j.plugin.processor.internal.ReachabilityMetadata;
+import org.apache.logging.log4j.util.Strings;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Java annotation processor that generates GraalVM metadata.
+ * <p>
+ * <strong>Note:</strong> The annotations listed here must also be classified by the {@link Annotations} helper.
+ * </p>
+ */
+@ServiceProvider(value = Processor.class, resolution = Resolution.OPTIONAL)
+@SupportedAnnotationTypes({
+ "org.apache.logging.log4j.plugins.Factory",
+ "org.apache.logging.log4j.plugins.PluginFactory",
+ "org.apache.logging.log4j.plugins.SingletonFactory",
+ "org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory",
+ "org.apache.logging.log4j.core.config.plugins.PluginFactory",
+ "org.apache.logging.log4j.plugins.Inject",
+ "org.apache.logging.log4j.plugins.Named",
+ "org.apache.logging.log4j.plugins.PluginAttribute",
+ "org.apache.logging.log4j.plugins.PluginBuilderAttribute",
+ "org.apache.logging.log4j.plugins.PluginElement",
+ "org.apache.logging.log4j.plugins.PluginNode",
+ "org.apache.logging.log4j.plugins.PluginValue",
+ "org.apache.logging.log4j.core.config.plugins.PluginAttribute",
+ "org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute",
+ "org.apache.logging.log4j.core.config.plugins.PluginConfiguration",
+ "org.apache.logging.log4j.core.config.plugins.PluginElement",
+ "org.apache.logging.log4j.core.config.plugins.PluginLoggerContext",
+ "org.apache.logging.log4j.core.config.plugins.PluginNode",
+ "org.apache.logging.log4j.core.config.plugins.PluginValue",
+ "org.apache.logging.log4j.plugins.Plugin",
+ "org.apache.logging.log4j.core.config.plugins.Plugin",
+ "org.apache.logging.log4j.plugins.condition.Conditional",
+ "org.apache.logging.log4j.plugins.validation.Constraint"
+})
+@SupportedOptions({"log4j.graalvm.groupId", "log4j.graalvm.artifactId"})
+public class GraalVmProcessor extends AbstractProcessor {
+
+ static final String GROUP_ID = "log4j.graalvm.groupId";
+ static final String ARTIFACT_ID = "log4j.graalvm.artifactId";
+ private static final String LOCATION_PREFIX = "META-INF/native-image/log4j-generated/";
+ private static final String LOCATION_SUFFIX = "/reflect-config.json";
+ private static final String PROCESSOR_NAME = GraalVmProcessor.class.getSimpleName();
+
+ private final Map<String, ReachabilityMetadata.Type> reachableTypes = new HashMap<>();
+ private final List<Element> processedElements = new ArrayList<>();
+ private Annotations annotationUtil;
+
+ @Override
+ public synchronized void init(ProcessingEnvironment processingEnv) {
+ super.init(processingEnv);
+ this.annotationUtil = new Annotations(processingEnv.getElementUtils());
+ }
+
+ @Override
+ public SourceVersion getSupportedSourceVersion() {
+ return SourceVersion.latest();
+ }
+
+ @Override
+ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
+ Messager messager = processingEnv.getMessager();
+ for (TypeElement annotation : annotations) {
+ Annotations.Type annotationType = annotationUtil.classifyAnnotation(annotation);
+ for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) {
+ switch (annotationType) {
+ case INJECT:
+ processInject(element);
+ break;
+ case PLUGIN:
+ processPlugin(element);
+ break;
+ case META_ANNOTATION_STRATEGY:
+ processMetaAnnotationStrategy(element, annotation);
+ break;
+ case QUALIFIER:
+ processQualifier(element);
+ break;
+ case FACTORY:
+ processFactory(element);
+ break;
+ case UNKNOWN:
+ messager.printMessage(
+ Diagnostic.Kind.WARNING,
+ String.format(
+ "The annotation type `%s` is not handled by %s", annotation, PROCESSOR_NAME),
+ annotation);
+ }
+ processedElements.add(element);
+ }
+ }
+ // Write the result file
+ if (roundEnv.processingOver() && !reachableTypes.isEmpty()) {
+ writeReachabilityMetadata();
+ }
+ // Do not claim the annotations to allow other annotation processors to run
+ return false;
+ }
+
+ private void processInject(Element element) {
+ if (element instanceof ExecutableElement executableElement) {
+ var parent = safeCast(executableElement.getEnclosingElement(), TypeElement.class);
+ addMethod(parent, executableElement);
+ } else if (element instanceof VariableElement variableElement) {
+ var parent = safeCast(variableElement.getEnclosingElement(), TypeElement.class);
+ addField(parent, variableElement);
+ }
+ }
+
+ private void processPlugin(Element element) {
+ TypeElement typeElement = safeCast(element, TypeElement.class);
+ for (Element child : typeElement.getEnclosedElements()) {
+ if (child instanceof ExecutableElement executableChild) {
+ if (executableChild.getModifiers().contains(Modifier.PUBLIC)) {
+ switch (executableChild.getSimpleName().toString()) {
+ // 1. All public constructors.
+ case "<init>":
+ addMethod(typeElement, executableChild);
+ break;
+ // 2. Static `newInstance` method used in, e.g. `PatternConverter` classes.
+ case "newInstance":
+ if (executableChild.getModifiers().contains(Modifier.STATIC)) {
+ addMethod(typeElement, executableChild);
+ }
+ break;
+ // 3. Other factory methods are annotated, so we don't deal with them here.
+ default:
+ }
+ }
+ }
+ }
+ }
+
+ private void processMetaAnnotationStrategy(Element element, TypeElement annotation) {
+ // Add the metadata for the public constructors
+ processPlugin(annotationUtil.getAnnotationClassValue(element, annotation));
+ }
+
+ private void processQualifier(Element element) {
+ if (element.getKind() == ElementKind.FIELD) {
+ addField(
+ safeCast(element.getEnclosingElement(), TypeElement.class),
+ safeCast(element, VariableElement.class));
+ }
+ }
+
+ private void processFactory(Element element) {
+ addMethod(
+ safeCast(element.getEnclosingElement(), TypeElement.class), safeCast(element, ExecutableElement.class));
+ }
+
+ private void writeReachabilityMetadata() {
+ // Compute the reachability metadata
+ ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream();
+ try {
+ ReachabilityMetadata.writeReflectConfig(reachableTypes.values(), arrayOutputStream);
+ } catch (IOException e) {
+ String message = String.format(
+ "%s: an error occurred while generating reachability metadata: %s", PROCESSOR_NAME, e.getMessage());
+ processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, message);
+ return;
+ }
+ byte[] data = arrayOutputStream.toByteArray();
+
+ Map<String, String> options = processingEnv.getOptions();
+ String reachabilityMetadataPath = getReachabilityMetadataPath(
+ options.get(GROUP_ID), options.get(ARTIFACT_ID), Integer.toHexString(Arrays.hashCode(data)));
+ Messager messager = processingEnv.getMessager();
+ messager.printMessage(
+ Diagnostic.Kind.NOTE,
+ String.format(
+ "%s: writing GraalVM metadata for %d Java classes to `%s`.",
+ PROCESSOR_NAME, reachableTypes.size(), reachabilityMetadataPath));
+ try (OutputStream output = processingEnv
+ .getFiler()
+ .createResource(
+ StandardLocation.CLASS_OUTPUT,
+ Strings.EMPTY,
+ reachabilityMetadataPath,
+ processedElements.toArray(Element[]::new))
+ .openOutputStream()) {
+ output.write(data);
+ } catch (IOException e) {
+ String message = String.format(
+ "%s: unable to write reachability metadata to file `%s`", PROCESSOR_NAME, reachabilityMetadataPath);
+ messager.printMessage(Diagnostic.Kind.ERROR, message);
+ throw new IllegalArgumentException(message, e);
+ }
+ }
+
+ /**
+ * Returns the path to the reachability metadata file.
+ * <p>
+ * If the groupId or artifactId is not specified, a warning is printed and a fallback folder name is used.
+ * The fallback folder name should be reproducible, but unique enough to avoid conflicts.
+ * </p>
+ *
+ * @param groupId The group ID of the plugin.
+ * @param artifactId The artifact ID of the plugin.
+ * @param fallbackFolderName The fallback folder name to use if groupId or artifactId is not specified.
+ */
+ String getReachabilityMetadataPath(
+ @Nullable String groupId, @Nullable String artifactId, String fallbackFolderName) {
+ if (groupId == null || artifactId == null) {
+ String message = String.format(
+ "The `%1$s` annotation processor is missing the recommended `%2$s` and `%3$s` options.%n"
+ + "To follow the GraalVM recommendations, please add the following options to your build tool:%n"
+ + " -A%2$s=<groupId>%n"
+ + " -A%3$s=<artifactId>%n",
+ PROCESSOR_NAME, GROUP_ID, ARTIFACT_ID);
+ processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, message);
+ return LOCATION_PREFIX + fallbackFolderName + LOCATION_SUFFIX;
+ }
+ return LOCATION_PREFIX + groupId + '/' + artifactId + LOCATION_SUFFIX;
+ }
+
+ private void addField(TypeElement parent, VariableElement element) {
+ ReachabilityMetadata.Type reachableType =
+ reachableTypes.computeIfAbsent(toString(parent), ReachabilityMetadata.Type::new);
+ reachableType.addField(
+ new ReachabilityMetadata.Field(element.getSimpleName().toString()));
+ }
+
+ private void addMethod(TypeElement parent, ExecutableElement element) {
+ ReachabilityMetadata.Type reachableType =
+ reachableTypes.computeIfAbsent(toString(parent), ReachabilityMetadata.Type::new);
+ ReachabilityMetadata.Method method =
+ new ReachabilityMetadata.Method(element.getSimpleName().toString());
+ element.getParameters().stream().map(v -> toString(v.asType())).forEach(method::addParameterType);
+ reachableType.addMethod(method);
+ }
+
+ private <T extends Element> T safeCast(Element element, Class<T> type) {
+ if (type.isInstance(element)) {
+ return type.cast(element);
+ }
+ // This should never happen, unless annotations start appearing on unexpected elements.
+ String msg = String.format(
+ "Unexpected type of element `%s`: expecting `%s` but found `%s`",
+ element, type.getName(), element.getClass().getName());
+ processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, msg, element);
+ throw new IllegalStateException(msg);
+ }
+
+ /**
+ * Returns the fully qualified name of a type.
+ *
+ * @param type A Java type.
+ */
+ private String toString(TypeMirror type) {
+ return type.accept(
+ new SimpleTypeVisitor8<String, @Nullable Void>() {
+ @Override
+ protected String defaultAction(final TypeMirror e, @Nullable Void unused) {
+ return e.toString();
+ }
+
+ @Override
+ public String visitArray(final ArrayType t, @Nullable Void unused) {
+ return visit(t.getComponentType(), unused) + "[]";
+ }
+
+ @Override
+ public @Nullable String visitDeclared(final DeclaredType t, final Void unused) {
+ return safeCast(t.asElement(), TypeElement.class)
+ .getQualifiedName()
+ .toString();
+ }
+ },
+ null);
+ }
+
+ /**
+ * Returns the fully qualified name of the element corresponding to a {@link DeclaredType}.
+ *
+ * @param element A Java language element.
+ */
+ private String toString(Element element) {
+ return element.accept(
+ new SimpleElementVisitor8<String, @Nullable Void>() {
+ @Override
+ public String visitPackage(PackageElement e, @Nullable Void unused) {
+ return e.getQualifiedName().toString();
+ }
+
+ @Override
+ public String visitType(TypeElement e, @Nullable Void unused) {
+ Element parent = e.getEnclosingElement();
+ String separator = parent.getKind() == ElementKind.PACKAGE ? "." : "$";
+ return visit(parent, unused)
+ + separator
+ + e.getSimpleName().toString();
+ }
+
+ @Override
+ protected String defaultAction(Element e, @Nullable Void unused) {
+ return "";
+ }
+ },
+ null);
+ }
+}
diff --git a/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/internal/Annotations.java b/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/internal/Annotations.java
new file mode 100644
index 0000000000..50e8a65105
--- /dev/null
+++ b/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/internal/Annotations.java
@@ -0,0 +1,149 @@
+/*
+ * 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.plugin.processor.internal;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.lang.model.element.AnnotationMirror;
+import javax.lang.model.element.AnnotationValue;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.util.Elements;
+import org.apache.logging.log4j.plugin.processor.GraalVmProcessor;
+
+public final class Annotations {
+
+ private static final Collection<String> FACTORY_TYPE_NAMES = List.of(
+ "org.apache.logging.log4j.plugins.Factory",
+ "org.apache.logging.log4j.plugins.PluginFactory",
+ "org.apache.logging.log4j.plugins.SingletonFactory",
+ "org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory",
+ "org.apache.logging.log4j.core.config.plugins.PluginFactory");
+
+ private static final Collection<String> INJECT_NAMES = List.of("org.apache.logging.log4j.plugins.Inject");
+
+ private static final Collection<String> QUALIFIER_TYPE_NAMES = List.of(
+ "org.apache.logging.log4j.plugins.Named",
+ "org.apache.logging.log4j.plugins.PluginAttribute",
+ "org.apache.logging.log4j.plugins.PluginBuilderAttribute",
+ "org.apache.logging.log4j.plugins.PluginElement",
+ "org.apache.logging.log4j.plugins.PluginNode",
+ "org.apache.logging.log4j.plugins.PluginValue",
+ "org.apache.logging.log4j.core.config.plugins.PluginAttribute",
+ "org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute",
+ "org.apache.logging.log4j.core.config.plugins.PluginConfiguration",
+ "org.apache.logging.log4j.core.config.plugins.PluginElement",
+ "org.apache.logging.log4j.core.config.plugins.PluginLoggerContext",
+ "org.apache.logging.log4j.core.config.plugins.PluginNode",
+ "org.apache.logging.log4j.core.config.plugins.PluginValue");
+
+ /**
+ * These must be public types with either:
+ * <ul>
+ * <li>A factory method.</li>
+ * <li>A static method called {@code newInstance}.</li>
+ * <li>A public no-argument constructor.</li>
+ * </ul>
+ * <p>
+ * <strong>Note:</strong> The annotations listed here must also be declared in
+ * {@link GraalVmProcessor}.
+ * </p>
+ */
+ private static final Collection<String> PLUGIN_ANNOTATION_NAMES =
+ List.of("org.apache.logging.log4j.plugins.Plugin", "org.apache.logging.log4j.core.config.plugins.Plugin");
+
+ /**
+ * Reflection is also used to create meta annotation strategies.
+ * .
+ * <p>
+ * <strong>Note:</strong> The annotations listed here must also be declared in
+ * {@link GraalVmProcessor}.
+ * </p>
+ */
+ private static final Collection<String> META_ANNOTATION_STRATEGY_NAMES = List.of(
+ "org.apache.logging.log4j.plugins.condition.Conditional",
+ "org.apache.logging.log4j.plugins.validation.Constraint");
+
+ public enum Type {
+ INJECT,
+ /**
+ * Annotation used to mark a configuration attribute, element or other injected parameters.
+ */
+ QUALIFIER,
+ /**
+ * Annotation used to mark a Log4j Plugin factory method.
+ */
+ FACTORY,
+ /**
+ * Annotation used to mark a Log4j Plugin class.
+ */
+ PLUGIN,
+ /**
+ * Annotation containing the name of a
+ * {@link org.apache.logging.log4j.plugins.validation.ConstraintValidator}
+ * or
+ * {@link org.apache.logging.log4j.plugins.condition.Condition}.
+ */
+ META_ANNOTATION_STRATEGY,
+ /**
+ * Unknown
+ */
+ UNKNOWN
+ }
+
+ private final Map<TypeElement, Type> typeElementToTypeMap = new HashMap<>();
+
+ public Annotations(final Elements elements) {
+ FACTORY_TYPE_NAMES.forEach(className -> addTypeElementIfExists(elements, className, Type.FACTORY));
+ INJECT_NAMES.forEach(className -> addTypeElementIfExists(elements, className, Type.INJECT));
+ QUALIFIER_TYPE_NAMES.forEach(className -> addTypeElementIfExists(elements, className, Type.QUALIFIER));
+ PLUGIN_ANNOTATION_NAMES.forEach(className -> addTypeElementIfExists(elements, className, Type.PLUGIN));
+ META_ANNOTATION_STRATEGY_NAMES.forEach(
+ className -> addTypeElementIfExists(elements, className, Type.META_ANNOTATION_STRATEGY));
+ }
+
+ private void addTypeElementIfExists(Elements elements, CharSequence className, Type type) {
+ final TypeElement element = elements.getTypeElement(className);
+ if (element != null) {
+ typeElementToTypeMap.put(element, type);
+ }
+ }
+
+ public Annotations.Type classifyAnnotation(TypeElement element) {
+ return typeElementToTypeMap.getOrDefault(element, Type.UNKNOWN);
+ }
+
+ public Element getAnnotationClassValue(Element element, TypeElement annotation) {
+ // This prevents getting an "Attempt to access Class object for TypeMirror" exception
+ AnnotationMirror annotationMirror = element.getAnnotationMirrors().stream()
+ .filter(am -> am.getAnnotationType().asElement().equals(annotation))
+ .findFirst()
+ .orElseThrow(
+ () -> new IllegalStateException("No `@" + annotation + "` annotation found on " + element));
+ AnnotationValue annotationValue = annotationMirror.getElementValues().entrySet().stream()
+ .filter(e -> "value".equals(e.getKey().getSimpleName().toString()))
+ .map(Map.Entry::getValue)
+ .findFirst()
+ .orElseThrow(() ->
+ new IllegalStateException("No `value` found `@" + annotation + "` annotation on " + element));
+ DeclaredType value = (DeclaredType) annotationValue.getValue();
+ return value.asElement();
+ }
+}
diff --git a/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/internal/ReachabilityMetadata.java b/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/internal/ReachabilityMetadata.java
new file mode 100644
index 0000000000..c65fcff721
--- /dev/null
+++ b/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/internal/ReachabilityMetadata.java
@@ -0,0 +1,296 @@
+/*
+ * 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.plugin.processor.internal;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.TreeSet;
+import java.util.stream.IntStream;
+import org.apache.logging.log4j.util.StringBuilders;
+import org.jspecify.annotations.NullMarked;
+
+/**
+ * Provides support for the
+ * <a href="https://www.graalvm.org/latest/reference-manual/native-image/metadata/#specifying-metadata-with-json">{@code reachability-metadata.json}</a>
+ * file format.
+ */
+@NullMarked
+public final class ReachabilityMetadata {
+
+ /**
+ * Key used to specify the name of a field or method
+ */
+ public static final String FIELD_OR_METHOD_NAME = "name";
+ /**
+ * Key used to list the method parameter types.
+ */
+ public static final String PARAMETER_TYPES = "parameterTypes";
+ /**
+ * Key used to specify the name of a type.
+ * <p>
+ * Since GraalVM for JDK 23 it will be called "type".
+ * </p>
+ */
+ public static final String TYPE_NAME = "name";
+ /**
+ * Key used to specify the list of fields available for reflection.
+ */
+ public static final String FIELDS = "fields";
+ /**
+ * Key used to specify the list of methods available for reflection.
+ */
+ public static final String METHODS = "methods";
+
+ private static class MinimalJsonWriter {
+ private final Appendable output;
+
+ public MinimalJsonWriter(Appendable output) {
+ this.output = output;
+ }
+
+ public void writeString(CharSequence input) throws IOException {
+ output.append('"');
+ StringBuilder sb = new StringBuilder(input);
+ StringBuilders.escapeJson(sb, 0);
+ output.append(sb);
+ output.append('"');
+ }
+
+ public void writeObjectStart() throws IOException {
+ output.append('{');
+ }
+
+ public void writeObjectEnd() throws IOException {
+ output.append('}');
+ }
+
+ public void writeObjectKey(CharSequence key) throws IOException {
+ writeString(key);
+ output.append(':').append(' ');
+ }
+
+ public void writeArrayStart() throws IOException {
+ output.append('[');
+ }
+
+ public void writeSeparator() throws IOException {
+ output.append(',').append(' ');
+ }
+
+ public void writeArrayEnd() throws IOException {
+ output.append(']');
+ }
+
+ public void writeLineSeparator() throws IOException {
+ output.append('\n');
+ }
+ }
+
+ /**
+ * Specifies a field that needs to be accessed through reflection.
+ */
+ public static final class Field implements Comparable<Field> {
+
+ private final String name;
+
+ public Field(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ void toJson(MinimalJsonWriter jsonWriter) throws IOException {
+ jsonWriter.writeObjectStart();
+ jsonWriter.writeObjectKey(FIELD_OR_METHOD_NAME);
+ jsonWriter.writeString(name);
+ jsonWriter.writeObjectEnd();
+ }
+
+ @Override
+ public int compareTo(Field other) {
+ return name.compareTo(other.name);
+ }
+ }
+
+ /**
+ * Specifies a method that needs to be accessed through reflection.
+ */
+ public static final class Method implements Comparable<Method> {
+
+ private final String name;
+ private final List<String> parameterTypes = new ArrayList<>();
+
+ public Method(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void addParameterType(final String parameterType) {
+ parameterTypes.add(parameterType);
+ }
+
+ void toJson(MinimalJsonWriter jsonWriter) throws IOException {
+ jsonWriter.writeObjectStart();
+ jsonWriter.writeObjectKey(FIELD_OR_METHOD_NAME);
+ jsonWriter.writeString(name);
+ jsonWriter.writeSeparator();
+ jsonWriter.writeObjectKey(PARAMETER_TYPES);
+ jsonWriter.writeArrayStart();
+ boolean first = true;
+ for (String parameterType : parameterTypes) {
+ if (!first) {
+ jsonWriter.writeSeparator();
+ }
+ first = false;
+ jsonWriter.writeString(parameterType);
+ }
+ jsonWriter.writeArrayEnd();
+ jsonWriter.writeObjectEnd();
+ }
+
+ @Override
+ public int compareTo(Method other) {
+ int result = name.compareTo(other.name);
+ if (result == 0) {
+ result = parameterTypes.size() - other.parameterTypes.size();
+ }
+ if (result == 0) {
+ result = IntStream.range(0, parameterTypes.size())
+ .map(idx -> parameterTypes.get(idx).compareTo(other.parameterTypes.get(idx)))
+ .filter(r -> r != 0)
+ .findFirst()
+ .orElse(0);
+ }
+ return result;
+ }
+ }
+
+ /**
+ * Specifies a Java type that needs to be accessed through reflection.
+ */
+ public static final class Type {
+
+ private final String type;
+ private final Collection<Method> methods = new TreeSet<>();
+ private final Collection<Field> fields = new TreeSet<>();
+
+ public Type(String type) {
+ this.type = type;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void addMethod(Method method) {
+ methods.add(method);
+ }
+
+ public void addField(Field field) {
+ fields.add(field);
+ }
+
+ void toJson(MinimalJsonWriter jsonWriter) throws IOException {
+ jsonWriter.writeObjectStart();
+ jsonWriter.writeObjectKey(TYPE_NAME);
+ jsonWriter.writeString(type);
+ jsonWriter.writeSeparator();
+
+ boolean first = true;
+ jsonWriter.writeObjectKey(METHODS);
+ jsonWriter.writeArrayStart();
+ for (Method method : methods) {
+ if (!first) {
+ jsonWriter.writeSeparator();
+ }
+ first = false;
+ method.toJson(jsonWriter);
+ }
+ jsonWriter.writeArrayEnd();
+ jsonWriter.writeSeparator();
+
+ first = true;
+ jsonWriter.writeObjectKey(FIELDS);
+ jsonWriter.writeArrayStart();
+ for (Field field : fields) {
+ if (!first) {
+ jsonWriter.writeSeparator();
+ }
+ first = false;
+ field.toJson(jsonWriter);
+ }
+ jsonWriter.writeArrayEnd();
+ jsonWriter.writeObjectEnd();
+ }
+ }
+
+ /**
+ * Collection of reflection metadata.
+ */
+ public static final class Reflection {
+
+ private final Collection<Type> types = new TreeSet<>(Comparator.comparing(Type::getType));
+
+ public Reflection(Collection<Type> types) {
+ this.types.addAll(types);
+ }
+
+ void toJson(MinimalJsonWriter jsonWriter) throws IOException {
+ boolean first = true;
+ jsonWriter.writeArrayStart();
+ for (Type type : types) {
+ if (!first) {
+ jsonWriter.writeSeparator();
+ }
+ first = false;
+ jsonWriter.writeLineSeparator();
+ type.toJson(jsonWriter);
+ }
+ jsonWriter.writeLineSeparator();
+ jsonWriter.writeArrayEnd();
+ }
+ }
+
+ /**
+ * Writes the contents of a {@code reflect-config.json} file.
+ *
+ * @param types The reflection metadata for types.
+ * @param output The object to use as output.
+ */
+ public static void writeReflectConfig(Collection<Type> types, OutputStream output) throws IOException {
+ try (Writer writer = new OutputStreamWriter(output, StandardCharsets.UTF_8)) {
+ Reflection reflection = new Reflection(types);
+ MinimalJsonWriter jsonWriter = new MinimalJsonWriter(writer);
+ reflection.toJson(jsonWriter);
+ jsonWriter.writeLineSeparator();
+ }
+ }
+
+ private ReachabilityMetadata() {}
+}
diff --git a/log4j-plugin-processor/src/test/java/org/apache/logging/log4j/plugin/processor/GraalVmProcessorTest.java b/log4j-plugin-processor/src/test/java/org/apache/logging/log4j/plugin/processor/GraalVmProcessorTest.java
new file mode 100644
index 0000000000..9e11f0be93
--- /dev/null
+++ b/log4j-plugin-processor/src/test/java/org/apache/logging/log4j/plugin/processor/GraalVmProcessorTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.plugin.processor;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Objects;
+import javax.tools.StandardLocation;
+import javax.tools.ToolProvider;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class GraalVmProcessorTest {
+ static final String GROUP_ID = "org.apache.logging.log4j";
+ static final String ARTIFACT_ID = "log4j-plugin-processor-test";
+ static final Path REFLECT_CONFIG_PATH =
+ Path.of("META-INF", "native-image", "log4j-generated", GROUP_ID, ARTIFACT_ID, "reflect-config.json");
+
+ static String readExpectedReflectConfig() throws IOException {
+ var url = Objects.requireNonNull(GraalVmProcessorTest.class.getResource("/expected-reflect-config.json"));
+ try (var inputStream = url.openStream()) {
+ return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
+ }
+ }
+
+ static String readActualReflectConfig(Path baseDirectory) throws IOException {
+ return Files.readString(baseDirectory.resolve(REFLECT_CONFIG_PATH));
+ }
+
+ static List<Path> findInputSourceFiles() throws IOException {
+ try (var stream = Files.list(Path.of("src", "test", "resources", "example"))) {
+ return stream.filter(Files::isRegularFile).toList();
+ }
+ }
+
+ @Test
+ void verifyAnnotationProcessorGeneratesExpectedReachability(@TempDir Path outputDir) throws Exception {
+ var compiler = ToolProvider.getSystemJavaCompiler();
+ var fileManager = compiler.getStandardFileManager(null, null, null);
+ fileManager.setLocationFromPaths(StandardLocation.CLASS_OUTPUT, List.of(outputDir));
+ fileManager.setLocationFromPaths(StandardLocation.SOURCE_OUTPUT, List.of(outputDir));
+ var sourceFiles = fileManager.getJavaFileObjectsFromPaths(findInputSourceFiles());
+ var options = List.of("-Alog4j.graalvm.groupId=" + GROUP_ID, "-Alog4j.graalvm.artifactId=" + ARTIFACT_ID);
+ var task = compiler.getTask(null, fileManager, null, options, null, sourceFiles);
+ task.setProcessors(List.of(new GraalVmProcessor()));
+ assertEquals(true, task.call());
+ String expected = readExpectedReflectConfig();
+ String actual = readActualReflectConfig(outputDir);
+ assertEquals(expected, actual);
+ }
+}
diff --git a/log4j-plugin-processor/src/test/resources/example/AbstractPluginWithGenericBuilder.java b/log4j-plugin-processor/src/test/resources/example/AbstractPluginWithGenericBuilder.java
new file mode 100644
index 0000000000..8350441b24
--- /dev/null
+++ b/log4j-plugin-processor/src/test/resources/example/AbstractPluginWithGenericBuilder.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package example;
+
+import org.apache.logging.log4j.plugins.PluginBuilderAttribute;
+import org.apache.logging.log4j.plugins.validation.constraints.Required;
+
+/**
+ *
+ */
+public class AbstractPluginWithGenericBuilder {
+
+ public abstract static class Builder<B extends Builder<B>> {
+
+ @PluginBuilderAttribute
+ @Required(message = "The thing given by the builder is null")
+ private String thing;
+
+ @SuppressWarnings("unchecked")
+ public B asBuilder() {
+ return (B) this;
+ }
+
+ public String getThing() {
+ return thing;
+ }
+
+ public B setThing(final String name) {
+ this.thing = name;
+ return asBuilder();
+ }
+ }
+
+ private final String thing;
+
+ public AbstractPluginWithGenericBuilder(final String thing) {
+ super();
+ this.thing = thing;
+ }
+
+ public String getThing() {
+ return thing;
+ }
+}
diff --git a/log4j-plugin-processor/src/test/resources/example/ConfigurablePlugin.java b/log4j-plugin-processor/src/test/resources/example/ConfigurablePlugin.java
new file mode 100644
index 0000000000..512a5eb4f8
--- /dev/null
+++ b/log4j-plugin-processor/src/test/resources/example/ConfigurablePlugin.java
@@ -0,0 +1,63 @@
+/*
+ * 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 example;
+
+import org.apache.logging.log4j.plugins.Configurable;
+import org.apache.logging.log4j.plugins.Inject;
+import org.apache.logging.log4j.plugins.Plugin;
+import org.apache.logging.log4j.plugins.PluginElement;
+
+@Configurable
+@Plugin("configurable")
+public class ConfigurablePlugin {
+ private final ValidatingPlugin alpha;
+ private final ValidatingPluginWithGenericBuilder beta;
+ private final ValidatingPluginWithTypedBuilder gamma;
+ private final PluginWithGenericSubclassFoo1Builder delta;
+
+ @Inject
+ public ConfigurablePlugin(
+ @PluginElement final ValidatingPlugin alpha,
+ @PluginElement final ValidatingPluginWithGenericBuilder beta,
+ @PluginElement final ValidatingPluginWithTypedBuilder gamma,
+ @PluginElement final PluginWithGenericSubclassFoo1Builder delta) {
+ this.alpha = alpha;
+ this.beta = beta;
+ this.gamma = gamma;
+ this.delta = delta;
+ }
+
+ public String getAlphaName() {
+ return alpha.getName();
+ }
+
+ public String getBetaName() {
+ return beta.getName();
+ }
+
+ public String getGammaName() {
+ return gamma.getName();
+ }
+
+ public String getDeltaThing() {
+ return delta.getThing();
+ }
+
+ public String getDeltaName() {
+ return delta.getFoo1();
+ }
+}
diff --git a/log4j-plugin-processor/src/test/resources/example/ConfigurableRecord.java b/log4j-plugin-processor/src/test/resources/example/ConfigurableRecord.java
new file mode 100644
index 0000000000..ce69006fdf
--- /dev/null
+++ b/log4j-plugin-processor/src/test/resources/example/ConfigurableRecord.java
@@ -0,0 +1,33 @@
+/*
+ * 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 example;
+
+import org.apache.logging.log4j.plugins.Configurable;
+import org.apache.logging.log4j.plugins.Inject;
+import org.apache.logging.log4j.plugins.Plugin;
+import org.apache.logging.log4j.plugins.PluginElement;
+
+@Configurable
+@Plugin
+public record ConfigurableRecord(
+ @PluginElement ValidatingPlugin alpha,
+ @PluginElement ValidatingPluginWithGenericBuilder beta,
+ @PluginElement ValidatingPluginWithTypedBuilder gamma,
+ @PluginElement PluginWithGenericSubclassFoo1Builder delta) {
+ @Inject
+ public ConfigurableRecord {}
+}
diff --git a/log4j-plugin-processor/src/test/resources/example/PluginWithGenericSubclassFoo1Builder.java b/log4j-plugin-processor/src/test/resources/example/PluginWithGenericSubclassFoo1Builder.java
new file mode 100644
index 0000000000..64a28433f0
--- /dev/null
+++ b/log4j-plugin-processor/src/test/resources/example/PluginWithGenericSubclassFoo1Builder.java
@@ -0,0 +1,66 @@
+/*
+ * 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 example;
+
+import org.apache.logging.log4j.plugins.Configurable;
+import org.apache.logging.log4j.plugins.Plugin;
+import org.apache.logging.log4j.plugins.PluginAttribute;
+import org.apache.logging.log4j.plugins.PluginFactory;
+import org.apache.logging.log4j.plugins.validation.constraints.Required;
+
+@Configurable
+@Plugin("PluginWithGenericSubclassFoo1Builder")
+public class PluginWithGenericSubclassFoo1Builder extends AbstractPluginWithGenericBuilder {
+
+ public static class Builder<B extends Builder<B>> extends AbstractPluginWithGenericBuilder.Builder<B>
+ implements org.apache.logging.log4j.plugins.util.Builder<PluginWithGenericSubclassFoo1Builder> {
+
+ @PluginAttribute
+ @Required(message = "The foo1 given by the builder is null")
+ private String foo1;
+
+ @Override
+ public PluginWithGenericSubclassFoo1Builder build() {
+ return new PluginWithGenericSubclassFoo1Builder(getThing(), getFoo1());
+ }
+
+ public String getFoo1() {
+ return foo1;
+ }
+
+ public B setFoo1(final String foo1) {
+ this.foo1 = foo1;
+ return asBuilder();
+ }
+ }
+
+ @PluginFactory
+ public static <B extends Builder<B>> B newBuilder() {
+ return new Builder<B>().asBuilder();
+ }
+
+ private final String foo1;
+
+ public PluginWithGenericSubclassFoo1Builder(final String thing, final String foo1) {
+ super(thing);
+ this.foo1 = foo1;
+ }
+
+ public String getFoo1() {
+ return foo1;
+ }
+}
diff --git a/log4j-plugin-processor/src/test/resources/example/ValidatingPlugin.java b/log4j-plugin-processor/src/test/resources/example/ValidatingPlugin.java
new file mode 100644
index 0000000000..3b48000c93
--- /dev/null
+++ b/log4j-plugin-processor/src/test/resources/example/ValidatingPlugin.java
@@ -0,0 +1,64 @@
+/*
+ * 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 example;
+
+import java.util.Objects;
+import org.apache.logging.log4j.plugins.Configurable;
+import org.apache.logging.log4j.plugins.Plugin;
+import org.apache.logging.log4j.plugins.PluginBuilderAttribute;
+import org.apache.logging.log4j.plugins.PluginFactory;
+import org.apache.logging.log4j.plugins.validation.constraints.Required;
+
+/**
+ *
+ */
+@Configurable
+@Plugin("Validator")
+public class ValidatingPlugin {
+
+ private final String name;
+
+ public ValidatingPlugin(final String name) {
+ this.name = Objects.requireNonNull(name, "name");
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ @PluginFactory
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ public static class Builder implements org.apache.logging.log4j.plugins.util.Builder<ValidatingPlugin> {
+
+ @PluginBuilderAttribute
+ @Required(message = "The name given by the builder is null")
+ private String name;
+
+ public Builder setName(final String name) {
+ this.name = name;
+ return this;
+ }
+
+ @Override
+ public ValidatingPlugin build() {
+ return new ValidatingPlugin(name);
+ }
+ }
+}
diff --git a/log4j-plugin-processor/src/test/resources/example/ValidatingPluginWithGenericBuilder.java b/log4j-plugin-processor/src/test/resources/example/ValidatingPluginWithGenericBuilder.java
new file mode 100644
index 0000000000..211a003fe3
--- /dev/null
+++ b/log4j-plugin-processor/src/test/resources/example/ValidatingPluginWithGenericBuilder.java
@@ -0,0 +1,70 @@
+/*
+ * 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 example;
+
+import java.util.Objects;
+import org.apache.logging.log4j.plugins.Configurable;
+import org.apache.logging.log4j.plugins.Plugin;
+import org.apache.logging.log4j.plugins.PluginAttribute;
+import org.apache.logging.log4j.plugins.PluginFactory;
+import org.apache.logging.log4j.plugins.validation.constraints.Required;
+
+/**
+ *
+ */
+@Configurable
+@Plugin("ValidatingPluginWithGenericBuilder")
+public class ValidatingPluginWithGenericBuilder {
+
+ private final String name;
+
+ public ValidatingPluginWithGenericBuilder(final String name) {
+ this.name = Objects.requireNonNull(name, "name");
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ @PluginFactory
+ public static <B extends Builder<B>> B newBuilder() {
+ return new Builder<B>().asBuilder();
+ }
+
+ public static class Builder<B extends Builder<B>>
+ implements org.apache.logging.log4j.plugins.util.Builder<ValidatingPluginWithGenericBuilder> {
+
+ @PluginAttribute
+ @Required(message = "The name given by the builder is null")
+ private String name;
+
+ public B setName(final String name) {
+ this.name = name;
+ return asBuilder();
+ }
+
+ @SuppressWarnings("unchecked")
+ public B asBuilder() {
+ return (B) this;
+ }
+
+ @Override
+ public ValidatingPluginWithGenericBuilder build() {
+ return new ValidatingPluginWithGenericBuilder(name);
+ }
+ }
+}
diff --git a/log4j-plugin-processor/src/test/resources/example/ValidatingPluginWithTypedBuilder.java b/log4j-plugin-processor/src/test/resources/example/ValidatingPluginWithTypedBuilder.java
new file mode 100644
index 0000000000..2977b1041a
--- /dev/null
+++ b/log4j-plugin-processor/src/test/resources/example/ValidatingPluginWithTypedBuilder.java
@@ -0,0 +1,65 @@
+/*
+ * 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 example;
+
+import java.util.Objects;
+import org.apache.logging.log4j.plugins.Configurable;
+import org.apache.logging.log4j.plugins.Plugin;
+import org.apache.logging.log4j.plugins.PluginBuilderAttribute;
+import org.apache.logging.log4j.plugins.PluginFactory;
+import org.apache.logging.log4j.plugins.validation.constraints.Required;
+
+/**
+ *
+ */
+@Configurable
+@Plugin("ValidatingPluginWithTypedBuilder")
+public class ValidatingPluginWithTypedBuilder {
+
+ private final String name;
+
+ public ValidatingPluginWithTypedBuilder(final String name) {
+ this.name = Objects.requireNonNull(name, "name");
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ @PluginFactory
+ public static Builder<Integer> newBuilder() {
+ return new Builder<>();
+ }
+
+ public static class Builder<T>
+ implements org.apache.logging.log4j.plugins.util.Builder<ValidatingPluginWithTypedBuilder> {
+
+ @PluginBuilderAttribute
+ @Required(message = "The name given by the builder is null")
+ private String name;
+
+ public Builder<T> setName(final String name) {
+ this.name = name;
+ return this;
+ }
+
+ @Override
+ public ValidatingPluginWithTypedBuilder build() {
+ return new ValidatingPluginWithTypedBuilder(name);
+ }
+ }
+}
diff --git a/log4j-plugin-processor/src/test/resources/expected-reflect-config.json b/log4j-plugin-processor/src/test/resources/expected-reflect-config.json
new file mode 100644
index 0000000000..f5af8c6511
--- /dev/null
+++ b/log4j-plugin-processor/src/test/resources/expected-reflect-config.json
@@ -0,0 +1,15 @@
+[
+{"name": "example.AbstractPluginWithGenericBuilder$Builder", "methods": [], "fields": [{"name": "thing"}]},
+{"name": "example.ConfigurablePlugin", "methods": [{"name": "<init>", "parameterTypes": ["example.ValidatingPlugin", "example.ValidatingPluginWithGenericBuilder", "example.ValidatingPluginWithTypedBuilder", "example.PluginWithGenericSubclassFoo1Builder"]}], "fields": []},
+{"name": "example.ConfigurableRecord", "methods": [{"name": "<init>", "parameterTypes": ["example.ValidatingPlugin", "example.ValidatingPluginWithGenericBuilder", "example.ValidatingPluginWithTypedBuilder", "example.PluginWithGenericSubclassFoo1Builder"]}], "fields": [{"name": "alpha"}, {"name": "beta"}, {"name": "delta"}, {"name": "gamma"}]},
+{"name": "example.FakePlugin", "methods": [{"name": "<init>", "parameterTypes": []}], "fields": []},
+{"name": "example.FakePlugin$Nested", "methods": [{"name": "<init>", "parameterTypes": []}], "fields": []},
+{"name": "example.PluginWithGenericSubclassFoo1Builder", "methods": [{"name": "<init>", "parameterTypes": ["java.lang.String", "java.lang.String"]}, {"name": "newBuilder", "parameterTypes": []}], "fields": []},
+{"name": "example.PluginWithGenericSubclassFoo1Builder$Builder", "methods": [], "fields": [{"name": "foo1"}]},
+{"name": "example.ValidatingPlugin", "methods": [{"name": "<init>", "parameterTypes": ["java.lang.String"]}, {"name": "newBuilder", "parameterTypes": []}], "fields": []},
+{"name": "example.ValidatingPlugin$Builder", "methods": [], "fields": [{"name": "name"}]},
+{"name": "example.ValidatingPluginWithGenericBuilder", "methods": [{"name": "<init>", "parameterTypes": ["java.lang.String"]}, {"name": "newBuilder", "parameterTypes": []}], "fields": []},
+{"name": "example.ValidatingPluginWithGenericBuilder$Builder", "methods": [], "fields": [{"name": "name"}]},
+{"name": "example.ValidatingPluginWithTypedBuilder", "methods": [{"name": "<init>", "parameterTypes": ["java.lang.String"]}, {"name": "newBuilder", "parameterTypes": []}], "fields": []},
+{"name": "example.ValidatingPluginWithTypedBuilder$Builder", "methods": [], "fields": [{"name": "name"}]}
+]
(logging-log4j2) 01/03: Extract class PluginIndex
Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
mattsicker pushed a commit to branch feature/3.x/graalvm-reachability
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
commit d4829f94c1731f1e548071e129d97940c5e7eb2c
Author: Matt Sicker <ms...@apple.com>
AuthorDate: Wed Aug 13 17:54:37 2025 -0500
Extract class PluginIndex
---
.../{PluginCacheTest.java => PluginIndexTest.java} | 40 ++++------
.../logging/log4j/plugins/model/PluginCache.java | 91 ----------------------
.../logging/log4j/plugins/model/PluginIndex.java | 79 +++++++++++++++++++
.../log4j/plugins/model/PluginRegistry.java | 30 ++++++-
4 files changed, 121 insertions(+), 119 deletions(-)
diff --git a/log4j-plugins-test/src/test/java/org/apache/logging/log4j/plugins/processor/PluginCacheTest.java b/log4j-plugins-test/src/test/java/org/apache/logging/log4j/plugins/processor/PluginIndexTest.java
similarity index 50%
rename from log4j-plugins-test/src/test/java/org/apache/logging/log4j/plugins/processor/PluginCacheTest.java
rename to log4j-plugins-test/src/test/java/org/apache/logging/log4j/plugins/processor/PluginIndexTest.java
index 6fc33cab54..a338e40d21 100644
--- a/log4j-plugins-test/src/test/java/org/apache/logging/log4j/plugins/processor/PluginCacheTest.java
+++ b/log4j-plugins-test/src/test/java/org/apache/logging/log4j/plugins/processor/PluginIndexTest.java
@@ -17,47 +17,39 @@
package org.apache.logging.log4j.plugins.processor;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertIterableEquals;
-import java.io.IOException;
-import java.util.Arrays;
import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import org.apache.logging.log4j.plugins.model.PluginCache;
import org.apache.logging.log4j.plugins.model.PluginEntry;
+import org.apache.logging.log4j.plugins.model.PluginIndex;
import org.junit.jupiter.api.Test;
import org.junitpioneer.jupiter.Issue;
-public class PluginCacheTest {
+public class PluginIndexTest {
@Test
@Issue("https://issues.apache.org/jira/browse/LOG4J2-2735")
- public void testOutputIsReproducibleWhenInputOrderingChanges() throws IOException {
- final PluginCache cacheA = new PluginCache();
- createCategory(cacheA, "one", Arrays.asList("bravo", "alpha", "charlie"));
- createCategory(cacheA, "two", Arrays.asList("alpha", "charlie", "bravo"));
- assertEquals(cacheA.getAllNamespaces().size(), 2);
- assertEquals(cacheA.getAllNamespaces().get("one").size(), 3);
- assertEquals(cacheA.getAllNamespaces().get("two").size(), 3);
- final PluginCache cacheB = new PluginCache();
- createCategory(cacheB, "two", Arrays.asList("bravo", "alpha", "charlie"));
- createCategory(cacheB, "one", Arrays.asList("alpha", "charlie", "bravo"));
- assertEquals(cacheB.getAllNamespaces().size(), 2);
- assertEquals(cacheB.getAllNamespaces().get("one").size(), 3);
- assertEquals(cacheB.getAllNamespaces().get("two").size(), 3);
- assertEquals(Objects.toString(cacheA.getAllNamespaces()), Objects.toString(cacheB.getAllNamespaces()));
+ public void testOutputIsReproducibleWhenInputOrderingChanges() {
+ final PluginIndex indexA = new PluginIndex();
+ createNamespace(indexA, "one", List.of("bravo", "alpha", "charlie"));
+ createNamespace(indexA, "two", List.of("alpha", "charlie", "bravo"));
+ assertEquals(6, indexA.size());
+ final PluginIndex indexB = new PluginIndex();
+ createNamespace(indexB, "two", List.of("bravo", "alpha", "charlie"));
+ createNamespace(indexB, "one", List.of("alpha", "charlie", "bravo"));
+ assertEquals(6, indexB.size());
+ assertIterableEquals(indexA, indexB);
}
- private void createCategory(final PluginCache cache, final String categoryName, final List<String> entryNames) {
- final Map<String, PluginEntry> category = cache.getNamespace(categoryName);
+ private void createNamespace(final PluginIndex index, final String namespace, final List<String> entryNames) {
for (String entryName : entryNames) {
final PluginEntry entry = PluginEntry.builder()
.setKey(entryName)
.setName(entryName)
.setClassName("com.example.Plugin")
- .setNamespace(categoryName)
+ .setNamespace(namespace)
.get();
- category.put(entryName, entry);
+ index.add(entry);
}
}
}
diff --git a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/model/PluginCache.java b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/model/PluginCache.java
deleted file mode 100644
index 024e931d3a..0000000000
--- a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/model/PluginCache.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * 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.plugins.model;
-
-import java.io.BufferedInputStream;
-import java.io.DataInputStream;
-import java.io.IOException;
-import java.net.URL;
-import java.util.Enumeration;
-import java.util.Locale;
-import java.util.Map;
-import java.util.TreeMap;
-
-public class PluginCache {
- private final Map<String, Map<String, PluginEntry>> namespaces = new TreeMap<>();
-
- /**
- * Returns all namespaces of plugins in this cache.
- *
- * @return all namespaces of plugins in this cache.
- * @since 2.1
- */
- public Map<String, Map<String, PluginEntry>> getAllNamespaces() {
- return namespaces;
- }
-
- /**
- * Gets or creates a namespace of plugins.
- *
- * @param namespace namespace to look up.
- * @return plugin mapping of names to plugin entries.
- */
- public Map<String, PluginEntry> getNamespace(final String namespace) {
- final String key = namespace.toLowerCase(Locale.ROOT);
- return namespaces.computeIfAbsent(key, ignored -> new TreeMap<>());
- }
-
- /**
- * Loads and merges all the Log4j plugin cache files specified. Usually, this is obtained via a ClassLoader.
- *
- * @param resources URLs to all the desired plugin cache files to load.
- * @throws IOException if an I/O exception occurs.
- */
- public void loadCacheFiles(final Enumeration<URL> resources) throws IOException {
- namespaces.clear();
- while (resources.hasMoreElements()) {
- final URL url = resources.nextElement();
- try (final DataInputStream in = new DataInputStream(new BufferedInputStream(url.openStream()))) {
- final int count = in.readInt();
- for (int i = 0; i < count; i++) {
- final var builder = PluginEntry.builder().setNamespace(in.readUTF());
- final Map<String, PluginEntry> m = getNamespace(builder.getNamespace());
- final int entries = in.readInt();
- for (int j = 0; j < entries; j++) {
- // Must always read all parts of the entry, even if not adding, so that the stream progresses
- final var entry = builder.setKey(in.readUTF())
- .setClassName(in.readUTF())
- .setName(in.readUTF())
- .setPrintable(in.readBoolean())
- .setDeferChildren(in.readBoolean())
- .get();
- m.putIfAbsent(entry.key(), entry);
- }
- }
- }
- }
- }
-
- /**
- * Gets the number of plugin namespaces registered.
- *
- * @return number of plugin namespaces in cache.
- */
- public int size() {
- return namespaces.size();
- }
-}
diff --git a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/model/PluginIndex.java b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/model/PluginIndex.java
new file mode 100644
index 0000000000..f9a6b38aca
--- /dev/null
+++ b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/model/PluginIndex.java
@@ -0,0 +1,79 @@
+/*
+ * 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.plugins.model;
+
+import java.util.AbstractCollection;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.function.Consumer;
+import org.jspecify.annotations.NullMarked;
+
+@NullMarked
+public class PluginIndex extends AbstractCollection<PluginEntry> {
+ private final Map<String, Map<String, PluginEntry>> index = new TreeMap<>();
+
+ @Override
+ public void forEach(Consumer<? super PluginEntry> action) {
+ for (var namespace : index.values()) {
+ for (var pluginEntry : namespace.values()) {
+ action.accept(pluginEntry);
+ }
+ }
+ }
+
+ @Override
+ public Iterator<PluginEntry> iterator() {
+ return index.values().stream()
+ .map(Map::values)
+ .flatMap(Collection::stream)
+ .iterator();
+ }
+
+ @Override
+ public int size() {
+ return index.values().stream().mapToInt(Map::size).sum();
+ }
+
+ @Override
+ public boolean add(PluginEntry entry) {
+ return getOrCreateNamespace(entry.namespace()).putIfAbsent(entry.key(), entry) == null;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return index.isEmpty();
+ }
+
+ @Override
+ public boolean contains(Object o) {
+ return o instanceof PluginEntry entry
+ && index.containsKey(entry.namespace())
+ && index.get(entry.namespace()).containsKey(entry.key())
+ && index.get(entry.namespace()).get(entry.key()).equals(entry);
+ }
+
+ @Override
+ public void clear() {
+ index.clear();
+ }
+
+ private Map<String, PluginEntry> getOrCreateNamespace(String namespace) {
+ return index.computeIfAbsent(namespace, ignored -> new TreeMap<>());
+ }
+}
diff --git a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/model/PluginRegistry.java b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/model/PluginRegistry.java
index dbd4fdc8b0..e09b510988 100644
--- a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/model/PluginRegistry.java
+++ b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/model/PluginRegistry.java
@@ -20,6 +20,8 @@ import static org.apache.logging.log4j.util.Unbox.box;
import aQute.bnd.annotation.Cardinality;
import aQute.bnd.annotation.spi.ServiceConsumer;
+import java.io.BufferedInputStream;
+import java.io.DataInputStream;
import java.io.IOException;
import java.net.URL;
import java.text.DecimalFormat;
@@ -101,24 +103,44 @@ public class PluginRegistry {
private Namespaces decodeCacheFiles(final ClassLoader classLoader) {
final long startTime = System.nanoTime();
- final PluginCache cache = new PluginCache();
+ final PluginIndex index = new PluginIndex();
try {
final Enumeration<URL> resources = classLoader.getResources(PLUGIN_CACHE_FILE);
if (resources == null) {
LOGGER.info("Plugin preloads not available from class loader {}", classLoader);
} else {
- cache.loadCacheFiles(resources);
+ while (resources.hasMoreElements()) {
+ final URL url = resources.nextElement();
+ try (final DataInputStream in = new DataInputStream(new BufferedInputStream(url.openStream()))) {
+ final int count = in.readInt();
+ for (int i = 0; i < count; i++) {
+ final var builder = PluginEntry.builder().setNamespace(in.readUTF());
+ final int entries = in.readInt();
+ for (int j = 0; j < entries; j++) {
+ // Must always read all parts of the entry, even if not adding, so that the stream
+ // progresses
+ final var entry = builder.setKey(in.readUTF())
+ .setClassName(in.readUTF())
+ .setName(in.readUTF())
+ .setPrintable(in.readBoolean())
+ .setDeferChildren(in.readBoolean())
+ .get();
+ index.add(entry);
+ }
+ }
+ }
+ }
}
} catch (final IOException ioe) {
LOGGER.warn("Unable to preload plugins", ioe);
}
final Namespaces namespaces = new Namespaces();
final AtomicInteger pluginCount = new AtomicInteger();
- cache.getAllNamespaces().forEach((key, outer) -> outer.values().forEach(entry -> {
+ index.forEach(entry -> {
final PluginType<?> type = new PluginType<>(entry, classLoader);
namespaces.add(type);
pluginCount.incrementAndGet();
- }));
+ });
reportLoadTime(classLoader, startTime, pluginCount);
return namespaces;
}