diff --git a/log4j-parent/pom.xml b/log4j-parent/pom.xml
index 1f1b6a75759..e2f940675c6 100644
--- a/log4j-parent/pom.xml
+++ b/log4j-parent/pom.xml
@@ -866,6 +866,9 @@
-Alog4j.docgen.version=${project.version}
-Alog4j.docgen.description=${project.description}
-Alog4j.docgen.typeFilter.excludePattern=${log4j.docgen.typeFilter.excludePattern}
+
+ -Alog4j.graalvm.groupId=${project.groupId}
+ -Alog4j.graalvm.artifactId=${project.artifactId}
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 00000000000..0f67a29f898
--- /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.
+ *
+ * Note: The annotations listed here must also be classified by the {@link Annotations} helper.
+ *
+ */
+@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 reachableTypes = new HashMap<>();
+ private final List 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 "":
+ 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 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.
+ *
+ * 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.
+ *
+ *
+ * @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=%n"
+ + " -A%3$s=%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 safeCast(Element element, Class 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() {
+ @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() {
+ @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/PluginProcessor.java b/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/PluginProcessor.java
index 8293bac51bf..ff41a4fdc9b 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.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 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 @@
*
*/
@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 processedElements = new HashSet<>();
public PluginProcessor() {}
@@ -112,89 +112,124 @@ public SourceVersion getSupportedSourceVersion() {
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 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 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 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 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 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 @@ private void writeClassFile(final String pkg, final List list) {
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 @@ private void writeClassFile(final String pkg, final List list) {
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 @@ private void writeClassFile(final String pkg, final List list) {
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 @@ private static String getNamespace(final TypeElement e) {
.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 {
- @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 @@ private boolean isServiceConsumerClassPresent() {
return processingEnv.getElementUtils().getTypeElement("aQute.bnd.annotation.spi.ServiceConsumer") != null;
}
- private void handleOptions(Map 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 @@ private void handleOptions(Map options) {
this.enableBndAnnotations = isServiceConsumerClassPresent();
}
}
-
- /**
- * ElementVisitor to scan the PluginAliases annotation.
- */
- private final class PluginAliasesElementVisitor extends SimpleElementVisitor8, Plugin> {
-
- private PluginAliasesElementVisitor() {
- super(List.of());
- }
-
- @Override
- public Collection 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 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/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 00000000000..50e8a651055
--- /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 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 INJECT_NAMES = List.of("org.apache.logging.log4j.plugins.Inject");
+
+ private static final Collection 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:
+ *
+ * - A factory method.
+ * - A static method called {@code newInstance}.
+ * - A public no-argument constructor.
+ *
+ *
+ * Note: The annotations listed here must also be declared in
+ * {@link GraalVmProcessor}.
+ *
+ */
+ private static final Collection 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.
+ * .
+ *
+ * Note: The annotations listed here must also be declared in
+ * {@link GraalVmProcessor}.
+ *
+ */
+ private static final Collection 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 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 00000000000..c65fcff7219
--- /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
+ * {@code reachability-metadata.json}
+ * 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.
+ *
+ * Since GraalVM for JDK 23 it will be called "type".
+ *
+ */
+ 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 {
+
+ 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 {
+
+ private final String name;
+ private final List 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 methods = new TreeSet<>();
+ private final Collection 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 types = new TreeSet<>(Comparator.comparing(Type::getType));
+
+ public Reflection(Collection 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 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 00000000000..9e11f0be933
--- /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 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/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 b6c2e7033ed..d3d1f88ecef 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.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 @@ private static PluginService generatePluginService(String expectedPluginPackage,
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 @@ private static PluginService generatePluginService(String expectedPluginPackage,
// 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> 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 @@ public List> getDiagnostics() {
public void report(Diagnostic extends JavaFileObject> diagnostic) {
switch (diagnostic.getKind()) {
case ERROR:
- case WARNING:
case MANDATORY_WARNING:
diagnostics.add(diagnostic);
break;
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 00000000000..8350441b241
--- /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> {
+
+ @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 00000000000..512a5eb4f86
--- /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 00000000000..ce69006fdfe
--- /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 00000000000..64a28433f0b
--- /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> extends AbstractPluginWithGenericBuilder.Builder
+ implements org.apache.logging.log4j.plugins.util.Builder {
+
+ @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 newBuilder() {
+ return new Builder().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 00000000000..3b48000c938
--- /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 {
+
+ @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 00000000000..211a003fe30
--- /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 newBuilder() {
+ return new Builder().asBuilder();
+ }
+
+ public static class Builder>
+ implements org.apache.logging.log4j.plugins.util.Builder {
+
+ @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 00000000000..2977b1041a1
--- /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 newBuilder() {
+ return new Builder<>();
+ }
+
+ public static class Builder
+ implements org.apache.logging.log4j.plugins.util.Builder {
+
+ @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 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 00000000000..f5af8c6511d
--- /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": "", "parameterTypes": ["example.ValidatingPlugin", "example.ValidatingPluginWithGenericBuilder", "example.ValidatingPluginWithTypedBuilder", "example.PluginWithGenericSubclassFoo1Builder"]}], "fields": []},
+{"name": "example.ConfigurableRecord", "methods": [{"name": "", "parameterTypes": ["example.ValidatingPlugin", "example.ValidatingPluginWithGenericBuilder", "example.ValidatingPluginWithTypedBuilder", "example.PluginWithGenericSubclassFoo1Builder"]}], "fields": [{"name": "alpha"}, {"name": "beta"}, {"name": "delta"}, {"name": "gamma"}]},
+{"name": "example.FakePlugin", "methods": [{"name": "", "parameterTypes": []}], "fields": []},
+{"name": "example.FakePlugin$Nested", "methods": [{"name": "", "parameterTypes": []}], "fields": []},
+{"name": "example.PluginWithGenericSubclassFoo1Builder", "methods": [{"name": "", "parameterTypes": ["java.lang.String", "java.lang.String"]}, {"name": "newBuilder", "parameterTypes": []}], "fields": []},
+{"name": "example.PluginWithGenericSubclassFoo1Builder$Builder", "methods": [], "fields": [{"name": "foo1"}]},
+{"name": "example.ValidatingPlugin", "methods": [{"name": "", "parameterTypes": ["java.lang.String"]}, {"name": "newBuilder", "parameterTypes": []}], "fields": []},
+{"name": "example.ValidatingPlugin$Builder", "methods": [], "fields": [{"name": "name"}]},
+{"name": "example.ValidatingPluginWithGenericBuilder", "methods": [{"name": "", "parameterTypes": ["java.lang.String"]}, {"name": "newBuilder", "parameterTypes": []}], "fields": []},
+{"name": "example.ValidatingPluginWithGenericBuilder$Builder", "methods": [], "fields": [{"name": "name"}]},
+{"name": "example.ValidatingPluginWithTypedBuilder", "methods": [{"name": "", "parameterTypes": ["java.lang.String"]}, {"name": "newBuilder", "parameterTypes": []}], "fields": []},
+{"name": "example.ValidatingPluginWithTypedBuilder$Builder", "methods": [], "fields": [{"name": "name"}]}
+]
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 6fc33cab54d..a338e40d210 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 entryNames) {
- final Map category = cache.getNamespace(categoryName);
+ private void createNamespace(final PluginIndex index, final String namespace, final List 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 024e931d3a7..00000000000
--- 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> namespaces = new TreeMap<>();
-
- /**
- * Returns all namespaces of plugins in this cache.
- *
- * @return all namespaces of plugins in this cache.
- * @since 2.1
- */
- public Map> 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 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 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 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 00000000000..f9a6b38aca7
--- /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 {
+ private final Map> 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 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 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 dbd4fdc8b0c..e09b5109880 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 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 @@ private void loadPlugins(final ClassLoader classLoader, final Namespaces namespa
private Namespaces decodeCacheFiles(final ClassLoader classLoader) {
final long startTime = System.nanoTime();
- final PluginCache cache = new PluginCache();
+ final PluginIndex index = new PluginIndex();
try {
final Enumeration 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;
}