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 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 annotations, final RoundEnvironment roundEnv) { - handleOptions(processingEnv.getOptions()); - final Messager messager = processingEnv.getMessager(); - messager.printMessage(Kind.NOTE, "Processing Log4j annotations"); - try { - final Set 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 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 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 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 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; }