From e6e6dddb41502edcbb89f8d23800449c142279f9 Mon Sep 17 00:00:00 2001
From: "Piotr P. Karwasz" <piotr.github@karwasz.org>
Date: Sat, 30 Nov 2024 14:09:33 +0100
Subject: [PATCH 1/2] Add `log4j.plugin.enableBndAnnotations` option to
 `PluginProcessor`

This adds a `log4j.plugin.enableBndAnnotations` option to the `PluginProcessor`. Its default value is inferred from the compiler classpath.

We also rename the `pluginPackage` option to a more coherent `log4j.plugin.package` option.

Closes #3251
---
 log4j-parent/pom.xml                          |   2 +-
 log4j-plugin-processor/pom.xml                |  49 +++-
 .../plugin/processor/PluginProcessor.java     |  92 +++++--
 .../plugin/processor/PluginProcessorTest.java | 253 ++++++++++++++++++
 .../test/resources/example}/FakePlugin.java   |   4 +-
 .../plugin/processor/PluginProcessorTest.java | 110 --------
 .../3151_plugin_processor_bnd_annotations.xml |  11 +
 7 files changed, 384 insertions(+), 137 deletions(-)
 create mode 100644 log4j-plugin-processor/src/test/java/org/apache/logging/log4j/plugin/processor/PluginProcessorTest.java
 rename {log4j-plugins-test/src/main/java/org/apache/logging/log4j/plugins/test/validation => log4j-plugin-processor/src/test/resources/example}/FakePlugin.java (92%)
 delete mode 100644 log4j-plugins-test/src/test/java/org/apache/logging/log4j/plugin/processor/PluginProcessorTest.java
 create mode 100644 src/changelog/.3.x.x/3151_plugin_processor_bnd_annotations.xml

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 @@
                 <id>default-testCompile</id>
                 <configuration>
                   <compilerArgs combine.children="append">
-                    <arg>-ApluginPackage=${log4jPluginPackageForTests}</arg>
+                    <arg>-Alog4j.plugin.package=${log4jPluginPackageForTests}</arg>
                   </compilerArgs>
                 </configuration>
               </execution>
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 @@
   <name>Apache Log4j Plugin Processor</name>
   <description>Log4j Plugin Annotation Processor</description>
 
-  <properties>
-    <log4jParentDir>${basedir}/..</log4jParentDir>
-  </properties>
-
   <dependencies>
 
     <dependency>
@@ -47,6 +43,51 @@
       <artifactId>log4j-plugins</artifactId>
     </dependency>
 
+    <dependency>
+      <groupId>org.assertj</groupId>
+      <artifactId>assertj-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>commons-io</groupId>
+      <artifactId>commons-io</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-api</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.junit-pioneer</groupId>
+      <artifactId>junit-pioneer</artifactId>
+      <scope>test</scope>
+    </dependency>
+
   </dependencies>
 
+  <build>
+    <plugins>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>test-no-bnd-annotations</id>
+            <configuration>
+              <classpathDependencyExcludes>
+                <exclude>biz.aQute.bnd:biz.aQute.bnd.annotation</exclude>
+              </classpathDependencyExcludes>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+
+    </plugins>
+  </build>
+
 </project>
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.
+ * <p>
+ *     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:
+ * </p>
+ * <pre>
+ *     {@code <log4j.plugin.package>.plugins.Log4jPlugins}
+ * </pre>
+ * <p>
+ *     where {@code <log4j.plugin.package>} is the effective value of the {@link #PLUGIN_PACKAGE} option.
+ * </p>
  */
+@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.
+     * <p>
+     *     The default behavior depends on the presence of {@code biz.aQute.bnd.annotation} on the classpath.
+     * </p>
+     */
+    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}
+     * <p>
+     *     If absent, the value of this option is the common prefix of all Log4j Plugin classes.
+     * </p>
+     */
+    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<String> 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<? extends TypeElement> annotations, final RoundEnvironment roundEnv) {
-        final Map<String, String> 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<? extends Element> 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<PluginEntry> list = new ArrayList<>();
@@ -115,14 +145,12 @@ private void error(final CharSequence message) {
 
     private String collectPlugins(
             String packageName, final Iterable<? extends Element> elements, final List<PluginEntry> 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<PluginEntry> 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<String, String> 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<? extends JavaFileObject> 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<Diagnostic<? extends JavaFileObject>> 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<String> 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<JavaFileObject> {
+
+        private final List<Diagnostic<? extends JavaFileObject>> diagnostics = new ArrayList<>();
+
+        private WarningCollector() {}
+
+        public List<Diagnostic<? extends JavaFileObject>> getDiagnostics() {
+            return diagnostics;
+        }
+
+        @Override
+        public void report(Diagnostic<? extends JavaFileObject> 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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns="https://logging.apache.org/xml/ns"
+       xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
+       type="updated">
+  <issue id="3251" link="https://github.com/apache/logging-log4j2/issues/3251"/>
+  <description format="asciidoc">
+    Add `log4j.plugin.enableBndAnnotations` option to `PluginProcessor`.
+    This also renames the `pluginPackage` option to `log4j.plugin.package`.
+  </description>
+</entry>

From 0bc19dea5cd71cfb78c92387b19322ce973f9cf4 Mon Sep 17 00:00:00 2001
From: "Piotr P. Karwasz" <pkarwasz-github@apache.org>
Date: Tue, 25 Mar 2025 09:59:33 +0100
Subject: [PATCH 2/2] Look for BND annotation on classpath

The processor lives in the annotation processor path, so `Class.forName` does **not** check for the presence of the BND annotation on the classpath.
---
 .../log4j/plugin/processor/PluginProcessor.java        | 10 +++-------
 1 file changed, 3 insertions(+), 7 deletions(-)

diff --git a/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/PluginProcessor.java b/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/PluginProcessor.java
index 40f4f93c62f..8293bac51bf 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
@@ -315,13 +315,9 @@ 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 boolean isServiceConsumerClassPresent() {
+        // Looks for the presence of the annotation on the classpath, not the annotation processor path.
+        return processingEnv.getElementUtils().getTypeElement("aQute.bnd.annotation.spi.ServiceConsumer") != null;
     }
 
     private void handleOptions(Map<String, String> options) {