diff --git a/log4j-parent/pom.xml b/log4j-parent/pom.xml index c2b3a8e651e..0d45bf9a96f 100644 --- a/log4j-parent/pom.xml +++ b/log4j-parent/pom.xml @@ -865,7 +865,7 @@ default-testCompile - -ApluginPackage=${log4jPluginPackageForTests} + -Alog4j.plugin.package=${log4jPluginPackageForTests} diff --git a/log4j-plugin-processor/pom.xml b/log4j-plugin-processor/pom.xml index 26034033aa6..7722a065305 100644 --- a/log4j-plugin-processor/pom.xml +++ b/log4j-plugin-processor/pom.xml @@ -31,10 +31,6 @@ Apache Log4j Plugin Processor Log4j Plugin Annotation Processor - - ${basedir}/.. - - @@ -47,6 +43,51 @@ log4j-plugins + + org.assertj + assertj-core + test + + + + commons-io + commons-io + test + + + + org.junit.jupiter + junit-jupiter-api + test + + + + org.junit-pioneer + junit-pioneer + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + test-no-bnd-annotations + + + biz.aQute.bnd:biz.aQute.bnd.annotation + + + + + + + + + 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 947c0278c9b..40f4f93c62f 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 @@ -57,25 +57,56 @@ import org.apache.logging.log4j.plugins.PluginAliases; import org.apache.logging.log4j.plugins.model.PluginEntry; import org.apache.logging.log4j.util.Strings; +import org.jspecify.annotations.NullMarked; /** - * Annotation processor for pre-scanning Log4j plugins. This generates implementation classes extending - * {@link org.apache.logging.log4j.plugins.model.PluginService} with a list of {@link PluginEntry} instances - * discovered from plugin annotations. By default, this will use the most specific package name it can derive - * from where the annotated plugins are located in a subpackage {@code plugins}. The output base package name - * can be overridden via the {@code pluginPackage} annotation processor option. + * Annotation processor to generate a {@link org.apache.logging.log4j.plugins.model.PluginService} implementation. + *

+ * This generates a {@link org.apache.logging.log4j.plugins.model.PluginService} implementation with a list of + * {@link PluginEntry} instances. + * The fully qualified class name of the generated service is: + *

+ *
+ *     {@code .plugins.Log4jPlugins}
+ * 
+ *

+ * where {@code } is the effective value of the {@link #PLUGIN_PACKAGE} option. + *

*/ +@NullMarked @SupportedAnnotationTypes({"org.apache.logging.log4j.plugins.*", "org.apache.logging.log4j.core.config.plugins.*"}) @ServiceProvider(value = Processor.class, resolution = Resolution.OPTIONAL) public class PluginProcessor extends AbstractProcessor { - // TODO: this could be made more abstract to allow for compile-time and run-time plugin processing + /** + * Option name to enable or disable the generation of {@link aQute.bnd.annotation.spi.ServiceConsumer} annotations. + *

+ * The default behavior depends on the presence of {@code biz.aQute.bnd.annotation} on the classpath. + *

+ */ + public static final String ENABLE_BND_ANNOTATIONS = "log4j.plugin.enableBndAnnotations"; + + /** + * Option name to determine the package containing the generated {@link org.apache.logging.log4j.plugins.model.PluginService} + *

+ * If absent, the value of this option is the common prefix of all Log4j Plugin classes. + *

+ */ + public static final String PLUGIN_PACKAGE = "log4j.plugin.package"; private static final String SERVICE_FILE_NAME = "META-INF/services/org.apache.logging.log4j.plugins.model.PluginService"; + private boolean enableBndAnnotations; + private String packageName = ""; + public PluginProcessor() {} + @Override + public Set getSupportedOptions() { + return Set.of(ENABLE_BND_ANNOTATIONS, PLUGIN_PACKAGE); + } + @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latest(); @@ -83,15 +114,14 @@ public SourceVersion getSupportedSourceVersion() { @Override public boolean process(final Set annotations, final RoundEnvironment roundEnv) { - final Map options = processingEnv.getOptions(); - String packageName = options.get("pluginPackage"); + 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 false; + return true; } messager.printMessage(Kind.NOTE, "Retrieved " + elements.size() + " Plugin elements"); final List list = new ArrayList<>(); @@ -115,14 +145,12 @@ private void error(final CharSequence message) { private String collectPlugins( String packageName, final Iterable elements, final List list) { - final boolean calculatePackage = packageName == null; + final boolean calculatePackage = packageName.isEmpty(); final var pluginVisitor = new PluginElementVisitor(); final var pluginAliasesVisitor = new PluginAliasesElementVisitor(); for (final Element element : elements) { - final Plugin plugin = element.getAnnotation(Plugin.class); - if (plugin == null) { - continue; - } + // 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) { @@ -135,11 +163,11 @@ private String collectPlugins( private String calculatePackage(Element element, String packageName) { final Name name = processingEnv.getElementUtils().getPackageOf(element).getQualifiedName(); - if (name == null) { - return null; + if (name.isEmpty()) { + return ""; } final String pkgName = name.toString(); - if (packageName == null) { + if (packageName.isEmpty()) { return pkgName; } if (pkgName.length() == packageName.length()) { @@ -158,6 +186,7 @@ private void writeServiceFile(final String pkgName) throws IOException { .createResource(StandardLocation.CLASS_OUTPUT, Strings.EMPTY, SERVICE_FILE_NAME); 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)); } } @@ -167,12 +196,16 @@ private void writeClassFile(final String pkg, final List list) { try (final PrintWriter writer = createSourceFile(fqcn)) { writer.println("package " + pkg + ".plugins;"); writer.println(""); - writer.println("import aQute.bnd.annotation.Resolution;"); - writer.println("import aQute.bnd.annotation.spi.ServiceProvider;"); + if (enableBndAnnotations) { + writer.println("import aQute.bnd.annotation.Resolution;"); + writer.println("import aQute.bnd.annotation.spi.ServiceProvider;"); + } writer.println("import org.apache.logging.log4j.plugins.model.PluginEntry;"); writer.println("import org.apache.logging.log4j.plugins.model.PluginService;"); writer.println(""); - writer.println("@ServiceProvider(value = PluginService.class, resolution = Resolution.OPTIONAL)"); + if (enableBndAnnotations) { + writer.println("@ServiceProvider(value = PluginService.class, resolution = Resolution.OPTIONAL)"); + } writer.println("public class Log4jPlugins extends PluginService {"); writer.println(""); writer.println(" private static final PluginEntry[] ENTRIES = new PluginEntry[] {"); @@ -282,6 +315,25 @@ private String commonPrefix(final String str1, final String str2) { return str1.substring(0, minLength); } + private static boolean isServiceConsumerClassPresent() { + try { + Class.forName("aQute.bnd.annotation.spi.ServiceConsumer"); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + private void handleOptions(Map options) { + packageName = options.getOrDefault(PLUGIN_PACKAGE, ""); + String enableBndAnnotationsOption = options.get(ENABLE_BND_ANNOTATIONS); + if (enableBndAnnotationsOption != null) { + this.enableBndAnnotations = !"false".equals(enableBndAnnotationsOption); + } else { + this.enableBndAnnotations = isServiceConsumerClassPresent(); + } + } + /** * ElementVisitor to scan the PluginAliases annotation. */ 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 new file mode 100644 index 00000000000..b6c2e7033ed --- /dev/null +++ b/log4j-plugin-processor/src/test/java/org/apache/logging/log4j/plugin/processor/PluginProcessorTest.java @@ -0,0 +1,253 @@ +/* + * 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 java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assumptions.assumeThat; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +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; +import java.util.stream.Stream; +import javax.tools.Diagnostic; +import javax.tools.DiagnosticListener; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.StandardLocation; +import javax.tools.ToolProvider; +import org.apache.commons.io.FileUtils; +import org.apache.logging.log4j.plugins.model.PluginEntry; +import org.apache.logging.log4j.plugins.model.PluginNamespace; +import org.apache.logging.log4j.plugins.model.PluginService; +import org.apache.logging.log4j.plugins.model.PluginType; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junitpioneer.jupiter.Issue; + +class PluginProcessorTest { + + private static final String CORE_NAMESPACE = "Core"; + private static final String TEST_NAMESPACE = "Test"; + + private static PathClassLoader classLoader; + private static PluginService pluginService; + + @BeforeAll + static void setup() throws Exception { + classLoader = new PathClassLoader(); + pluginService = generatePluginService("example"); + } + + @AfterAll + static void cleanup() { + pluginService = null; + classLoader = null; + } + + private static PluginService generatePluginService(String expectedPluginPackage, String... options) + throws Exception { + // Source file + URL fakePluginUrl = PluginProcessorTest.class.getResource("/example/FakePlugin.java"); + assertThat(fakePluginUrl).isNotNull(); + Path fakePluginPath = Paths.get(fakePluginUrl.toURI()); + // Collect warnings + WarningCollector collector = new WarningCollector(); + String fqcn = expectedPluginPackage + ".plugins.Log4jPlugins"; + Path outputDir = Files.createTempDirectory("PluginProcessorTest"); + + try { + // Instantiate the tooling + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + StandardJavaFileManager fileManager = compiler.getStandardFileManager(collector, Locale.ROOT, UTF_8); + + // Populate sources + Iterable sources = fileManager.getJavaFileObjects(fakePluginPath); + + // Set the target path used by `DescriptorGenerator` to dump the generated files + fileManager.setLocationFromPaths(StandardLocation.CLASS_OUTPUT, Set.of(outputDir)); + fileManager.setLocationFromPaths(StandardLocation.SOURCE_OUTPUT, Set.of(outputDir)); + + // Compile the sources + final JavaCompiler.CompilationTask task = + compiler.getTask(null, fileManager, collector, Arrays.asList(options), null, sources); + task.setProcessors(List.of(new PluginProcessor())); + task.call(); + + // Verify successful compilation + List> diagnostics = collector.getDiagnostics(); + assertThat(diagnostics).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(); + } finally { + FileUtils.deleteDirectory(outputDir.toFile()); + } + } + + @Test + void namespaceFound() { + assertThat(pluginService.size()).as("Number of namespaces").isNotZero(); + assertThat(pluginService.getNamespace(CORE_NAMESPACE)) + .as("Namespace %s", CORE_NAMESPACE) + .isNotNull(); + } + + static Stream checkFakePluginInformation() { + return Stream.of("Fake", "AnotherFake", "StillFake"); + } + + @ParameterizedTest + @MethodSource + void checkFakePluginInformation(String aliasName) { + PluginNamespace namespace = pluginService.getNamespace(CORE_NAMESPACE); + assertThat(namespace).isNotNull(); + PluginType pluginType = namespace.get(aliasName); + assertThat(pluginType).as("Plugin type with alias `%s`", aliasName).isNotNull(); + verifyPluginEntry( + pluginType.getPluginEntry(), + aliasName.toLowerCase(Locale.ROOT), + CORE_NAMESPACE, + "Fake", + "example.FakePlugin", + "Fake", + true, + true); + } + + @Test + void checkNestedPluginInformation() { + PluginNamespace namespace = pluginService.getNamespace(TEST_NAMESPACE); + assertThat(namespace).isNotNull(); + PluginType pluginType = namespace.get("Nested"); + assertThat(pluginType).as("Plugin type with alias `%s`", "Nested").isNotNull(); + verifyPluginEntry( + pluginType.getPluginEntry(), + "nested", + TEST_NAMESPACE, + "Nested", + "example.FakePlugin$Nested", + "", + false, + false); + } + + @Test + void checkPluginPackageOption() throws Exception { + PluginService pluginService = generatePluginService("com.example", "-Alog4j.plugin.package=com.example"); + assertThat(pluginService).isNotNull(); + } + + @Test + void checkEnableBndAnnotationsOption() { + // If we don't have the annotations on the classpath compilation should fail + assumeThat(areBndAnnotationsAbsent()).isTrue(); + Assertions.assertThrows( + NullPointerException.class, + () -> generatePluginService( + "com.example.bnd", + "-Alog4j.plugin.package=com.example.bnd", + "-Alog4j.plugin.enableBndAnnotations=true")); + } + + private boolean areBndAnnotationsAbsent() { + try { + Class.forName("aQute.bnd.annotation.spi.ServiceConsumer"); + return false; + } catch (ClassNotFoundException e) { + return true; + } + } + + private void verifyPluginEntry( + PluginEntry actual, + String key, + String namespace, + String name, + String className, + String elementType, + boolean deferChildren, + boolean printable) { + assertThat(actual.key()).as("Key").isEqualTo(key); + assertThat(actual.namespace()).as("Namespace").isEqualTo(namespace); + assertThat(actual.name()).as("Name").isEqualTo(name); + assertThat(actual.className()).as("Class name").isEqualTo(className); + assertThat(actual.elementType()).as("Element type").isEqualTo(elementType); + assertThat(actual.deferChildren()).as("Deferred children").isEqualTo(deferChildren); + assertThat(actual.printable()).as("Printable").isEqualTo(printable); + } + + @Test + @Issue("https://github.com/apache/logging-log4j2/issues/1520") + public void testReproducibleOutputOrder() { + assertThat(pluginService.getEntries()).isSorted(); + } + + private static class WarningCollector implements DiagnosticListener { + + private final List> diagnostics = new ArrayList<>(); + + private WarningCollector() {} + + public List> getDiagnostics() { + return diagnostics; + } + + @Override + public void report(Diagnostic diagnostic) { + switch (diagnostic.getKind()) { + case ERROR: + case WARNING: + case MANDATORY_WARNING: + diagnostics.add(diagnostic); + break; + default: + } + } + } + + private static class PathClassLoader extends ClassLoader { + + public PathClassLoader() { + super(PluginProcessorTest.class.getClassLoader()); + } + + public Class defineClass(String name, Path path) throws IOException { + final byte[] bytes; + try (InputStream inputStream = Files.newInputStream(path)) { + bytes = inputStream.readAllBytes(); + } + return defineClass(name, bytes, 0, bytes.length); + } + } +} diff --git a/log4j-plugins-test/src/main/java/org/apache/logging/log4j/plugins/test/validation/FakePlugin.java b/log4j-plugin-processor/src/test/resources/example/FakePlugin.java similarity index 92% rename from log4j-plugins-test/src/main/java/org/apache/logging/log4j/plugins/test/validation/FakePlugin.java rename to log4j-plugin-processor/src/test/resources/example/FakePlugin.java index 87166b25f3f..decc8acf27e 100644 --- a/log4j-plugins-test/src/main/java/org/apache/logging/log4j/plugins/test/validation/FakePlugin.java +++ b/log4j-plugin-processor/src/test/resources/example/FakePlugin.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.logging.log4j.plugins.test.validation; +package example; import org.apache.logging.log4j.plugins.Configurable; import org.apache.logging.log4j.plugins.Namespace; @@ -24,7 +24,7 @@ /** * Test plugin class for unit tests. */ -@Configurable(deferChildren = true) +@Configurable(deferChildren = true, printObject = true) @Plugin("Fake") @PluginAliases({"AnotherFake", "StillFake"}) public class FakePlugin { diff --git a/log4j-plugins-test/src/test/java/org/apache/logging/log4j/plugin/processor/PluginProcessorTest.java b/log4j-plugins-test/src/test/java/org/apache/logging/log4j/plugin/processor/PluginProcessorTest.java deleted file mode 100644 index c1ecc9aa403..00000000000 --- a/log4j-plugins-test/src/test/java/org/apache/logging/log4j/plugin/processor/PluginProcessorTest.java +++ /dev/null @@ -1,110 +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.plugin.processor; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; - -import org.apache.logging.log4j.plugins.Configurable; -import org.apache.logging.log4j.plugins.Plugin; -import org.apache.logging.log4j.plugins.PluginAliases; -import org.apache.logging.log4j.plugins.di.Keys; -import org.apache.logging.log4j.plugins.model.PluginService; -import org.apache.logging.log4j.plugins.model.PluginType; -import org.apache.logging.log4j.plugins.test.validation.FakePlugin; -import org.apache.logging.log4j.plugins.test.validation.plugins.Log4jPlugins; -import org.junit.BeforeClass; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; -import org.junitpioneer.jupiter.Issue; - -@RunWith(JUnit4.class) -public class PluginProcessorTest { - - private static PluginService pluginService; - - private final Plugin p = FakePlugin.class.getAnnotation(Plugin.class); - private final Configurable c = FakePlugin.class.getAnnotation(Configurable.class); - private final String ns = Keys.getNamespace(FakePlugin.class); - - @BeforeClass - public static void setUpClass() { - pluginService = new Log4jPlugins(); - } - - @Test - public void testTestCategoryFound() throws Exception { - assertNotNull("No plugin annotation on FakePlugin.", p); - final var namespace = pluginService.getNamespace(ns); - assertNotEquals("No plugins were found.", 0, pluginService.size()); - assertNotNull("The namespace '" + ns + "' was not found.", namespace); - assertFalse(namespace.isEmpty()); - } - - @Test - public void testFakePluginFoundWithCorrectInformation() throws Exception { - final var testCategory = pluginService.getNamespace(ns); - assertNotNull(testCategory); - final PluginType type = testCategory.get(p.value()); - assertNotNull(type); - verifyFakePluginEntry(p.value(), type); - } - - @Test - public void testFakePluginAliasesContainSameInformation() throws Exception { - final PluginAliases aliases = FakePlugin.class.getAnnotation(PluginAliases.class); - for (final String alias : aliases.value()) { - final var testCategory = pluginService.getNamespace(ns); - assertNotNull(testCategory); - final PluginType type = testCategory.get(alias); - assertNotNull(type); - verifyFakePluginEntry(alias, type); - } - } - - private void verifyFakePluginEntry(final String name, final PluginType fake) { - assertNotNull("The plugin '" + name.toLowerCase() + "' was not found.", fake); - assertEquals(FakePlugin.class.getName(), fake.getPluginEntry().className()); - assertEquals(name.toLowerCase(), fake.getKey()); - assertEquals(Plugin.EMPTY, c.elementType()); - assertEquals(p.value(), fake.getName()); - assertEquals(c.printObject(), fake.isObjectPrintable()); - assertEquals(c.deferChildren(), fake.isDeferChildren()); - } - - @Test - public void testNestedPlugin() throws Exception { - final Plugin p = FakePlugin.Nested.class.getAnnotation(Plugin.class); - final var testCategory = pluginService.getNamespace(Keys.getNamespace(FakePlugin.Nested.class)); - assertNotNull(testCategory); - final PluginType nested = testCategory.get(p.value()); - assertNotNull(nested); - assertEquals(p.value().toLowerCase(), nested.getKey()); - assertEquals(FakePlugin.Nested.class.getName(), nested.getPluginEntry().className()); - assertEquals(p.value(), nested.getName()); - } - - @Test - @Issue("https://github.com/apache/logging-log4j2/issues/1520") - public void testReproducibleOutputOrder() { - assertThat(pluginService.getEntries()).isSorted(); - } -} diff --git a/src/changelog/.3.x.x/3151_plugin_processor_bnd_annotations.xml b/src/changelog/.3.x.x/3151_plugin_processor_bnd_annotations.xml new file mode 100644 index 00000000000..459f25876d5 --- /dev/null +++ b/src/changelog/.3.x.x/3151_plugin_processor_bnd_annotations.xml @@ -0,0 +1,11 @@ + + + + + Add `log4j.plugin.enableBndAnnotations` option to `PluginProcessor`. + This also renames the `pluginPackage` option to `log4j.plugin.package`. + +