This is an automated email from the ASF dual-hosted git repository.

mattsicker pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git

commit 5a0eafd10956738d8ab4ce00325651a29ec58bce
Author: Matt Sicker <[email protected]>
AuthorDate: Sat May 14 23:20:16 2022 -0500

    LOG4J2-3496 - Support injection via container types
    
    - Adds Category qualifier annotation type for injecting plugins from a 
category
    - Adds support for injecting Optional<P>, Collection<P>, Iterable<P>, 
Set<P>, Stream<P>, List<P>, and Map<String, P>, for a given category
    - Moves PluginRegistry into a singleton bean
    - Update OSGi activation to work with PluginRegistry
    
    Signed-off-by: Matt Sicker <[email protected]>
---
 .../apache/logging/log4j/util/ServiceRegistry.java |   4 +-
 .../apache/logging/log4j/core/osgi/Activator.java  |  34 +++++++
 .../plugins/test/validation/generic/AlphaBean.java |  31 ++++++
 .../plugins/test/validation/generic/BaseBean.java  |  25 +++++
 .../plugins/test/validation/generic/BetaBean.java  |  28 +++++
 .../plugins/test/validation/generic/GammaBean.java |  26 +++++
 .../logging/log4j/plugins/di/InjectorTest.java     |  35 +++++++
 .../org/apache/logging/log4j/plugins/Category.java |  42 ++++++++
 .../logging/log4j/plugins/di/DefaultInjector.java  | 113 +++++++++++++++++----
 .../log4j/plugins/name/CategoryNameProvider.java   |  30 ++++++
 .../logging/log4j/plugins/osgi/Activator.java      |  77 ++++++++------
 .../logging/log4j/plugins/util/PluginRegistry.java |  18 +---
 src/site/asciidoc/manual/dependencyinjection.adoc  |   1 +
 src/site/asciidoc/manual/plugins.adoc              |   4 +-
 14 files changed, 395 insertions(+), 73 deletions(-)

diff --git 
a/log4j-api/src/main/java/org/apache/logging/log4j/util/ServiceRegistry.java 
b/log4j-api/src/main/java/org/apache/logging/log4j/util/ServiceRegistry.java
index 2268ffaddd..83a40fdb73 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/util/ServiceRegistry.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/ServiceRegistry.java
@@ -105,7 +105,9 @@ public class ServiceRegistry {
      * @param <S>         type of service
      */
     public <S> void registerBundleServices(final Class<S> serviceType, final 
long bundleId, final List<S> services) {
-        bundleServices.computeIfAbsent(bundleId, ignored -> new 
ConcurrentHashMap<>()).put(serviceType, services);
+        bundleServices.computeIfAbsent(bundleId, ignored -> new 
ConcurrentHashMap<>())
+                .computeIfAbsent(serviceType, ignored -> new ArrayList<>())
+                .addAll(cast(services));
     }
 
     /**
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/osgi/Activator.java 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/osgi/Activator.java
index 332275d488..4549e63ca6 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/osgi/Activator.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/osgi/Activator.java
@@ -20,16 +20,22 @@ package org.apache.logging.log4j.core.osgi;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.core.util.Constants;
 import org.apache.logging.log4j.core.util.ContextDataProvider;
+import org.apache.logging.log4j.plugins.di.Injector;
 import org.apache.logging.log4j.plugins.di.InjectorCallback;
+import org.apache.logging.log4j.plugins.di.Key;
 import org.apache.logging.log4j.plugins.processor.PluginService;
+import org.apache.logging.log4j.plugins.util.PluginRegistry;
 import org.apache.logging.log4j.spi.Provider;
 import org.apache.logging.log4j.util.PropertiesUtil;
 import org.apache.logging.log4j.util.ServiceRegistry;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleActivator;
 import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
 import org.osgi.framework.wiring.BundleWiring;
 
+import java.util.Hashtable;
+import java.util.List;
 import java.util.concurrent.atomic.AtomicReference;
 
 /**
@@ -37,13 +43,33 @@ import java.util.concurrent.atomic.AtomicReference;
  */
 public final class Activator implements BundleActivator {
     private final AtomicReference<BundleContext> contextRef = new 
AtomicReference<>();
+    private ServiceRegistration<PluginRegistry> 
pluginRegistryServiceRegistration;
+    private PluginRegistry pluginRegistry;
+    private ServiceRegistration<InjectorCallback> 
injectorCallbackServiceRegistration;
+    private InjectorCallback injectorCallback;
 
     @Override
     public void start(final BundleContext context) throws Exception {
+        pluginRegistryServiceRegistration = 
context.registerService(PluginRegistry.class, new PluginRegistry(), new 
Hashtable<>());
+        pluginRegistry = 
context.getService(pluginRegistryServiceRegistration.getReference());
+        injectorCallbackServiceRegistration = 
context.registerService(InjectorCallback.class, new InjectorCallback() {
+            @Override
+            public void configure(final Injector injector) {
+                injector.registerBinding(Key.forClass(PluginRegistry.class),
+                        () -> 
context.getService(pluginRegistryServiceRegistration.getReference()));
+            }
+
+            @Override
+            public int getOrder() {
+                return -50;
+            }
+        }, new Hashtable<>());
+        injectorCallback = 
context.getService(injectorCallbackServiceRegistration.getReference());
         final ServiceRegistry registry = ServiceRegistry.getInstance();
         final Bundle bundle = context.getBundle();
         final long bundleId = bundle.getBundleId();
         final ClassLoader classLoader = 
bundle.adapt(BundleWiring.class).getClassLoader();
+        registry.registerBundleServices(InjectorCallback.class, bundleId, 
List.of(injectorCallback));
         registry.loadServicesFromBundle(PluginService.class, bundleId, 
classLoader);
         registry.loadServicesFromBundle(Provider.class, bundleId, classLoader);
         registry.loadServicesFromBundle(ContextDataProvider.class, bundleId, 
classLoader);
@@ -58,6 +84,14 @@ public final class Activator implements BundleActivator {
     @Override
     public void stop(final BundleContext context) throws Exception {
         
ServiceRegistry.getInstance().unregisterBundleServices(context.getBundle().getBundleId());
+        if (injectorCallback != null) {
+            injectorCallback = null;
+            injectorCallbackServiceRegistration.unregister();
+        }
+        if (pluginRegistry != null) {
+            pluginRegistry = null;
+            pluginRegistryServiceRegistration.unregister();
+        }
         this.contextRef.compareAndSet(context, null);
         LogManager.shutdown(false, true);
     }
diff --git 
a/log4j-plugins-test/src/main/java/org/apache/logging/log4j/plugins/test/validation/generic/AlphaBean.java
 
b/log4j-plugins-test/src/main/java/org/apache/logging/log4j/plugins/test/validation/generic/AlphaBean.java
new file mode 100644
index 0000000000..40ce717911
--- /dev/null
+++ 
b/log4j-plugins-test/src/main/java/org/apache/logging/log4j/plugins/test/validation/generic/AlphaBean.java
@@ -0,0 +1,31 @@
+/*
+ * 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.test.validation.generic;
+
+import org.apache.logging.log4j.plugins.Factory;
+import org.apache.logging.log4j.plugins.Plugin;
+import org.apache.logging.log4j.plugins.Singleton;
+
+@Plugin(category = "Bean", name = "Alpha")
+@Singleton
+public class AlphaBean implements BaseBean {
+    @Factory
+    static AlphaBean newAlphaBean() {
+        return new AlphaBean();
+    }
+}
diff --git 
a/log4j-plugins-test/src/main/java/org/apache/logging/log4j/plugins/test/validation/generic/BaseBean.java
 
b/log4j-plugins-test/src/main/java/org/apache/logging/log4j/plugins/test/validation/generic/BaseBean.java
new file mode 100644
index 0000000000..e481a085d9
--- /dev/null
+++ 
b/log4j-plugins-test/src/main/java/org/apache/logging/log4j/plugins/test/validation/generic/BaseBean.java
@@ -0,0 +1,25 @@
+/*
+ * 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.test.validation.generic;
+
+import org.apache.logging.log4j.plugins.Category;
+
+@Category("Bean")
+public
+interface BaseBean {
+}
diff --git 
a/log4j-plugins-test/src/main/java/org/apache/logging/log4j/plugins/test/validation/generic/BetaBean.java
 
b/log4j-plugins-test/src/main/java/org/apache/logging/log4j/plugins/test/validation/generic/BetaBean.java
new file mode 100644
index 0000000000..f872c47348
--- /dev/null
+++ 
b/log4j-plugins-test/src/main/java/org/apache/logging/log4j/plugins/test/validation/generic/BetaBean.java
@@ -0,0 +1,28 @@
+/*
+ * 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.test.validation.generic;
+
+import org.apache.logging.log4j.plugins.Plugin;
+import org.apache.logging.log4j.plugins.PluginOrder;
+import org.apache.logging.log4j.plugins.Singleton;
+
+@Plugin(category = "Bean", name = "Beta")
+@PluginOrder(PluginOrder.FIRST)
+@Singleton
+public class BetaBean implements BaseBean {
+}
diff --git 
a/log4j-plugins-test/src/main/java/org/apache/logging/log4j/plugins/test/validation/generic/GammaBean.java
 
b/log4j-plugins-test/src/main/java/org/apache/logging/log4j/plugins/test/validation/generic/GammaBean.java
new file mode 100644
index 0000000000..73519484d5
--- /dev/null
+++ 
b/log4j-plugins-test/src/main/java/org/apache/logging/log4j/plugins/test/validation/generic/GammaBean.java
@@ -0,0 +1,26 @@
+/*
+ * 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.test.validation.generic;
+
+import org.apache.logging.log4j.plugins.Plugin;
+import org.apache.logging.log4j.plugins.Singleton;
+
+@Plugin(category = "Bean", name = "Gamma")
+@Singleton
+public class GammaBean implements BaseBean {
+}
diff --git 
a/log4j-plugins-test/src/test/java/org/apache/logging/log4j/plugins/di/InjectorTest.java
 
b/log4j-plugins-test/src/test/java/org/apache/logging/log4j/plugins/di/InjectorTest.java
index 5fb1ba3f5d..7ce4484220 100644
--- 
a/log4j-plugins-test/src/test/java/org/apache/logging/log4j/plugins/di/InjectorTest.java
+++ 
b/log4j-plugins-test/src/test/java/org/apache/logging/log4j/plugins/di/InjectorTest.java
@@ -17,6 +17,7 @@
 
 package org.apache.logging.log4j.plugins.di;
 
+import org.apache.logging.log4j.plugins.Category;
 import org.apache.logging.log4j.plugins.Factory;
 import org.apache.logging.log4j.plugins.Inject;
 import org.apache.logging.log4j.plugins.Named;
@@ -31,6 +32,9 @@ import org.apache.logging.log4j.plugins.ScopeType;
 import org.apache.logging.log4j.plugins.Singleton;
 import org.apache.logging.log4j.plugins.processor.PluginEntry;
 import 
org.apache.logging.log4j.plugins.test.validation.ValidatingPluginWithGenericBuilder;
+import org.apache.logging.log4j.plugins.test.validation.generic.BaseBean;
+import org.apache.logging.log4j.plugins.test.validation.generic.BetaBean;
+import org.apache.logging.log4j.plugins.test.validation.generic.GammaBean;
 import org.apache.logging.log4j.plugins.util.PluginType;
 import org.apache.logging.log4j.plugins.util.TypeUtil;
 import org.apache.logging.log4j.plugins.validation.constraints.Required;
@@ -41,14 +45,17 @@ import org.junit.jupiter.api.parallel.Resources;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.Collection;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Function;
 import java.util.function.Supplier;
+import java.util.stream.Stream;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -789,4 +796,32 @@ class InjectorTest {
         assertThat(first.c).isNotNull().isNotSameAs(second.c);
         assertThat(second.c).isNotNull();
     }
+
+    /**
+     * @see <a 
href="https://issues.apache.org/jira/browse/LOG4J2-3496";>LOG4J2-3496</a>
+     */
+    static class ContainerPluginBeanInjection {
+        @Category("Bean") Optional<BaseBean> optional;
+        @Category("Bean") Collection<BaseBean> collection;
+        @Category("Bean") Iterable<BaseBean> iterable;
+        @Category("Bean") Set<BaseBean> set;
+        @Category("Bean") Stream<BaseBean> stream;
+        @Category("Bean") List<BaseBean> list;
+        @Category("Bean") Map<String, BaseBean> map;
+    }
+
+    @Test
+    void categoryQualifierInjection() {
+        final ContainerPluginBeanInjection instance = DI.createInjector()
+                .registerBinding(Keys.PLUGIN_PACKAGES_KEY, () -> 
List.of(BaseBean.class.getPackageName()))
+                .getInstance(ContainerPluginBeanInjection.class);
+        
assertThat(instance.list).hasSize(3).first().isInstanceOf(BetaBean.class);
+        
assertThat(instance.collection).containsExactlyElementsOf(instance.list);
+        assertThat(instance.iterable).containsExactlyElementsOf(instance.list);
+        assertThat(instance.set).containsExactlyElementsOf(instance.list);
+        assertThat(instance.stream).containsExactlyElementsOf(instance.list);
+        assertThat(instance.map).hasSize(3);
+        assertThat(instance.map.get("gamma")).isInstanceOf(GammaBean.class);
+        assertThat(instance.optional).get().isInstanceOf(BetaBean.class);
+    }
 }
diff --git 
a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/Category.java 
b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/Category.java
new file mode 100644
index 0000000000..7f7e47a5e8
--- /dev/null
+++ b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/Category.java
@@ -0,0 +1,42 @@
+/*
+ * 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;
+
+import org.apache.logging.log4j.plugins.name.CategoryNameProvider;
+import org.apache.logging.log4j.plugins.name.NameProvider;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Qualifier used for matching against injectable plugins in a given category.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE, 
ElementType.TYPE_USE })
+@Documented
+@QualifierType
+@NameProvider(CategoryNameProvider.class)
+public @interface Category {
+    /**
+     * The category to use plugins from.
+     */
+    String value();
+}
diff --git 
a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/DefaultInjector.java
 
b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/DefaultInjector.java
index fe2c0097d8..590ba47bd9 100644
--- 
a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/DefaultInjector.java
+++ 
b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/di/DefaultInjector.java
@@ -18,11 +18,13 @@
 package org.apache.logging.log4j.plugins.di;
 
 import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.plugins.Category;
 import org.apache.logging.log4j.plugins.FactoryType;
 import org.apache.logging.log4j.plugins.Inject;
 import org.apache.logging.log4j.plugins.Named;
 import org.apache.logging.log4j.plugins.Node;
 import org.apache.logging.log4j.plugins.PluginException;
+import org.apache.logging.log4j.plugins.PluginOrder;
 import org.apache.logging.log4j.plugins.QualifierType;
 import org.apache.logging.log4j.plugins.ScopeType;
 import org.apache.logging.log4j.plugins.Singleton;
@@ -250,21 +252,57 @@ class DefaultInjector implements Injector {
         if (existing != null) {
             return existing.getSupplier();
         }
+
         final Class<T> rawType = key.getRawType();
         final Scope scope = getScopeForType(rawType);
+
+        // @Named PluginCategory injection
         if (rawType == PluginCategory.class && key.getQualifierType() == 
Named.class) {
             final Key<PluginCategory> pluginCategoryKey = TypeUtil.cast(key);
             final Supplier<PluginCategory> pluginCategoryFactory = 
createPluginCategoryFactory(pluginCategoryKey);
-            bindingMap.put(pluginCategoryKey, pluginCategoryFactory);
+            bindingMap.putIfAbsent(pluginCategoryKey, pluginCategoryFactory);
+            return bindingMap.get(key, aliases).getSupplier();
+        }
+
+        // @Category Collection<T>/Map<String, T>/Stream<T> injection
+        if (key.getQualifierType() == Category.class) {
+            if (Stream.class.isAssignableFrom(rawType)) {
+                final Key<Stream<T>> streamKey = TypeUtil.cast(key);
+                final Supplier<Stream<T>> streamFactory =
+                        () -> 
streamPluginInstancesFromCategory(key.getParameterizedTypeArgument(0));
+                bindingMap.putIfAbsent(streamKey, streamFactory);
+            } else if (Set.class.isAssignableFrom(rawType)) {
+                final Key<Set<T>> setKey = TypeUtil.cast(key);
+                final Supplier<Set<T>> setFactory = () -> 
getPluginSet(key.getParameterizedTypeArgument(0));
+                bindingMap.putIfAbsent(setKey, setFactory);
+            } else if (Map.class.isAssignableFrom(rawType)) {
+                final Key<Map<String, T>> mapKey = TypeUtil.cast(key);
+                final Supplier<Map<String, T>> mapFactory = () -> 
getPluginMap(key.getParameterizedTypeArgument(1));
+                bindingMap.putIfAbsent(mapKey, mapFactory);
+            } else if (Iterable.class.isAssignableFrom(rawType)) {
+                final Key<Iterable<T>> iterableKey = TypeUtil.cast(key);
+                final Supplier<Iterable<T>> iterableFactory = () -> 
getPluginList(key.getParameterizedTypeArgument(0));
+                bindingMap.putIfAbsent(iterableKey, iterableFactory);
+            } else if (Optional.class.isAssignableFrom(rawType)) {
+                final Key<Optional<T>> optionalKey = TypeUtil.cast(key);
+                final Supplier<Optional<T>> optionalFactory = () -> 
getOptionalPlugin(key.getParameterizedTypeArgument(0));
+                bindingMap.putIfAbsent(optionalKey, optionalFactory);
+            } else {
+                throw new InjectException("Cannot inject plugins into " + key);
+            }
             return bindingMap.get(key, aliases).getSupplier();
         }
+
+        // Optional<T> injection
         if (rawType == Optional.class) {
             final Key<Optional<T>> optionalKey = TypeUtil.cast(key);
-            final Supplier<Optional<T>> optionalFactory =
-                    createOptionalFactory(key.getParameterizedTypeArgument(0), 
aliases, node, chain);
+            final Supplier<Optional<T>> optionalFactory = () ->
+                    getOptionalInstance(key.getParameterizedTypeArgument(0), 
aliases, node, chain);
             bindingMap.put(optionalKey, optionalFactory);
             return bindingMap.get(key, aliases).getSupplier();
         }
+
+        // generic T injection
         final Supplier<T> instanceSupplier = () -> {
             final StringBuilder debugLog = new StringBuilder();
             final T instance = TypeUtil.cast(getInjectableInstance(key, node, 
chain, debugLog));
@@ -275,24 +313,57 @@ class DefaultInjector implements Injector {
     }
 
     private Supplier<PluginCategory> createPluginCategoryFactory(final 
Key<PluginCategory> key) {
-        return LazyValue.from(() -> {
-            final String categoryName = key.getName();
-            final Binding<List<String>> pluginPackagesBinding = 
bindingMap.get(Keys.PLUGIN_PACKAGES_KEY, List.of());
-            final List<String> pluginPackages = pluginPackagesBinding != null 
? pluginPackagesBinding.getSupplier().get() : List.of();
-            // TODO: can this be a bean, too? might be tricky in OSGi
-            return PluginRegistry.getInstance().getCategory(categoryName, 
pluginPackages);
-        });
+        return LazyValue.from(() -> 
getInstance(PluginRegistry.class).getCategory(key.getName(), 
getPluginPackages()));
+    }
+
+    private List<String> getPluginPackages() {
+        final Binding<List<String>> pluginPackagesBinding = 
bindingMap.get(Keys.PLUGIN_PACKAGES_KEY, List.of());
+        return pluginPackagesBinding != null ? 
pluginPackagesBinding.getSupplier().get() : List.of();
+    }
+
+    private <T> Stream<PluginType<? extends T>> 
streamPluginsFromCategory(final Key<T> itemKey) {
+        if (itemKey == null) {
+            return Stream.empty();
+        }
+        final PluginCategory category = 
getInstance(PluginRegistry.class).getCategory(itemKey.getName(), 
getPluginPackages());
+        final Type type = itemKey.getType();
+        final Class<T> rawType = itemKey.getRawType();
+        return category.stream()
+                .filter(pluginType -> rawType.isInterface() && 
TypeUtil.isAssignable(type, pluginType.getPluginClass()) ||
+                        TypeUtil.isAssignable(type, 
pluginType.getPluginClass()))
+                .sorted(Comparator.comparing(PluginType::getPluginClass, 
PluginOrder.COMPARATOR))
+                .map(TypeUtil::cast);
     }
 
-    private <T> Supplier<Optional<T>> createOptionalFactory(
+    private <T> Stream<T> streamPluginInstancesFromCategory(final Key<T> key) {
+        return streamPluginsFromCategory(key).map(pluginType -> 
getInstance(pluginType.getPluginClass()));
+    }
+
+    private <T> Set<T> getPluginSet(final Key<T> key) {
+        return 
streamPluginInstancesFromCategory(key).collect(Collectors.toCollection(LinkedHashSet::new));
+    }
+
+    private <T> Map<String, T> getPluginMap(final Key<T> key) {
+        return streamPluginsFromCategory(key).collect(
+                Collectors.toMap(PluginType::getKey, pluginType -> 
getInstance(pluginType.getPluginClass()), (lhs, rhs) -> lhs,
+                        LinkedHashMap::new));
+    }
+
+    private <T> List<T> getPluginList(final Key<T> key) {
+        return 
streamPluginInstancesFromCategory(key).collect(Collectors.toList());
+    }
+
+    public <T> Optional<T> getOptionalPlugin(final Key<T> key) {
+        return streamPluginInstancesFromCategory(key).findFirst();
+    }
+
+    private <T> Optional<T> getOptionalInstance(
             final Key<T> key, final Collection<String> aliases, final Node 
node, final Set<Key<?>> chain) {
-        return () -> {
-            try {
-                return Optional.ofNullable(getFactory(key, aliases, node, 
chain).get());
-            } catch (final PluginException e) {
-                return Optional.empty();
-            }
-        };
+        try {
+            return Optional.ofNullable(getFactory(key, aliases, node, 
chain).get());
+        } catch (final PluginException e) {
+            return Optional.empty();
+        }
     }
 
     private Object getInjectableInstance(
@@ -361,13 +432,11 @@ class DefaultInjector implements Injector {
         if (conflictingConverter != null) {
             final boolean overridable;
             if (converter instanceof Comparable) {
-                @SuppressWarnings("unchecked")
-                final Comparable<TypeConverter<?>> comparableConverter =
+                @SuppressWarnings("unchecked") final 
Comparable<TypeConverter<?>> comparableConverter =
                         (Comparable<TypeConverter<?>>) converter;
                 overridable = 
comparableConverter.compareTo(conflictingConverter) < 0;
             } else if (conflictingConverter instanceof Comparable) {
-                @SuppressWarnings("unchecked")
-                final Comparable<TypeConverter<?>> 
comparableConflictingConverter =
+                @SuppressWarnings("unchecked") final 
Comparable<TypeConverter<?>> comparableConflictingConverter =
                         (Comparable<TypeConverter<?>>) conflictingConverter;
                 overridable = 
comparableConflictingConverter.compareTo(converter) > 0;
             } else {
diff --git 
a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/name/CategoryNameProvider.java
 
b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/name/CategoryNameProvider.java
new file mode 100644
index 0000000000..9772498072
--- /dev/null
+++ 
b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/name/CategoryNameProvider.java
@@ -0,0 +1,30 @@
+/*
+ * 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.name;
+
+import org.apache.logging.log4j.plugins.Category;
+import org.apache.logging.log4j.util.Strings;
+
+import java.util.Optional;
+
+public class CategoryNameProvider implements 
AnnotatedElementNameProvider<Category> {
+    @Override
+    public Optional<String> getSpecifiedName(final Category annotation) {
+        return Strings.trimToOptional(annotation.value());
+    }
+}
diff --git 
a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/osgi/Activator.java
 
b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/osgi/Activator.java
index 6ff109df5d..f17cd67d7c 100644
--- 
a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/osgi/Activator.java
+++ 
b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/osgi/Activator.java
@@ -44,34 +44,22 @@ public final class Activator implements BundleActivator, 
SynchronousBundleListen
     private static final Logger LOGGER = StatusLogger.getLogger();
 
     private static final SecurityManager SECURITY_MANAGER = 
System.getSecurityManager();
+    public static final String CORE_MODULE_NAME = 
"org.apache.logging.log4j.core";
 
     private final AtomicReference<BundleContext> contextRef = new 
AtomicReference<>();
 
+    private int state = Bundle.UNINSTALLED;
+
+    private ServiceReference<PluginRegistry> pluginRegistryServiceReference;
+    private PluginRegistry pluginRegistry;
+
     @Override
     public void start(final BundleContext bundleContext) throws Exception {
-        loadPlugins(bundleContext);
+        state = Bundle.STARTING;
         bundleContext.addBundleListener(this);
-        final Bundle[] bundles = bundleContext.getBundles();
-        for (final Bundle bundle : bundles) {
-            loadPlugins(bundle);
-        }
-        scanInstalledBundlesForPlugins(bundleContext);
         this.contextRef.compareAndSet(null, bundleContext);
     }
 
-    private void loadPlugins(final BundleContext bundleContext) {
-        final PluginRegistry pluginRegistry = PluginRegistry.getInstance();
-        try {
-            final Collection<ServiceReference<PluginService>> 
serviceReferences = bundleContext.getServiceReferences(PluginService.class, 
null);
-            for (final ServiceReference<PluginService> serviceReference : 
serviceReferences) {
-                final PluginService pluginService = 
bundleContext.getService(serviceReference);
-                
pluginRegistry.loadFromBundle(bundleContext.getBundle().getBundleId(), 
pluginService.getCategories());
-            }
-        } catch (final InvalidSyntaxException ex) {
-            LOGGER.error("Error accessing Plugins", ex);
-        }
-    }
-
     private void loadPlugins(final Bundle bundle) {
         if (bundle.getState() == Bundle.UNINSTALLED) {
             return;
@@ -83,10 +71,17 @@ public final class Activator implements BundleActivator, 
SynchronousBundleListen
             if (bundleContext == null) {
                 LOGGER.debug("Bundle {} has no context (state={}), skipping 
loading plugins", bundle.getSymbolicName(), toStateString(bundle.getState()));
             } else {
-                loadPlugins(bundleContext);
+                final Collection<ServiceReference<PluginService>> 
serviceReferences =
+                        
bundleContext.getServiceReferences(PluginService.class, null);
+                for (final ServiceReference<PluginService> serviceReference : 
serviceReferences) {
+                    final PluginService pluginService = 
bundleContext.getService(serviceReference);
+                    
pluginRegistry.loadFromBundle(bundleContext.getBundle().getBundleId(), 
pluginService.getCategories());
+                }
             }
         } catch (final SecurityException e) {
             LOGGER.debug("Cannot access bundle [{}] contents. Ignoring.", 
bundle.getSymbolicName(), e);
+        } catch (final InvalidSyntaxException ex) {
+            LOGGER.error("Error accessing Plugins", ex);
         } catch (final Exception e) {
             LOGGER.warn("Problem checking bundle {} for Log4j 2 provider.", 
bundle.getSymbolicName(), e);
         }
@@ -117,28 +112,28 @@ public final class Activator implements BundleActivator, 
SynchronousBundleListen
         }
     }
 
-    private static void scanInstalledBundlesForPlugins(final BundleContext 
context) {
+    private void scanInstalledBundlesForPlugins(final BundleContext context) {
         final Bundle[] bundles = context.getBundles();
         for (final Bundle bundle : bundles) {
-            // TODO: bundle state can change during this
             scanBundleForPlugins(bundle);
         }
     }
 
-    private static void scanBundleForPlugins(final Bundle bundle) {
+    private void scanBundleForPlugins(final Bundle bundle) {
         final long bundleId = bundle.getBundleId();
         // LOG4J2-920: don't scan system bundle for plugins
         if (bundle.getState() == Bundle.ACTIVE && bundleId != 0) {
             LOGGER.trace("Scanning bundle [{}, id={}] for plugins.", 
bundle.getSymbolicName(), bundleId);
             final ClassLoader classLoader = 
bundle.adapt(BundleWiring.class).getClassLoader();
-            PluginRegistry.getInstance().loadFromBundle(bundleId, classLoader);
+            pluginRegistry.loadFromBundle(bundleId, classLoader);
         }
     }
 
-    private static void stopBundlePlugins(final Bundle bundle) {
+    private void stopBundlePlugins(final Bundle bundle) {
         LOGGER.trace("Stopping bundle [{}] plugins.", 
bundle.getSymbolicName());
-        // TODO: plugin lifecycle code
-        PluginRegistry.getInstance().clearBundlePlugins(bundle.getBundleId());
+        if (pluginRegistry != null) {
+            pluginRegistry.clearBundlePlugins(bundle.getBundleId());
+        }
     }
 
     @Override
@@ -153,15 +148,33 @@ public final class Activator implements BundleActivator, 
SynchronousBundleListen
 
     @Override
     public void bundleChanged(final BundleEvent event) {
+        final Bundle bundle = event.getBundle();
         switch (event.getType()) {
-            // FIXME: STARTING instead of STARTED?
-            case BundleEvent.STARTED:
-                loadPlugins(event.getBundle());
-                scanBundleForPlugins(event.getBundle());
+            case BundleEvent.STARTING:
+                if (CORE_MODULE_NAME.equals(bundle.getSymbolicName()) && state 
!= Bundle.ACTIVE) {
+                    break;
+                }
                 break;
 
+            case BundleEvent.STARTED:
+                if (CORE_MODULE_NAME.equals(bundle.getSymbolicName()) && state 
!= Bundle.ACTIVE) {
+                    final BundleContext bundleContext = contextRef.get();
+                    pluginRegistryServiceReference =
+                            
bundleContext.getServiceReference(PluginRegistry.class);
+                    pluginRegistry = 
bundleContext.getService(pluginRegistryServiceReference);
+                    scanInstalledBundlesForPlugins(bundleContext);
+                    state = Bundle.ACTIVE;
+                } else if (state == Bundle.ACTIVE) {
+                    loadPlugins(bundle);
+                    scanBundleForPlugins(bundle);
+                }
+
             case BundleEvent.STOPPING:
-                stopBundlePlugins(event.getBundle());
+                if (CORE_MODULE_NAME.equals(bundle.getSymbolicName()) && 
pluginRegistry != null) {
+                    pluginRegistry = null;
+                    
contextRef.get().ungetService(pluginRegistryServiceReference);
+                }
+                stopBundlePlugins(bundle);
                 break;
 
             default:
diff --git 
a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/util/PluginRegistry.java
 
b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/util/PluginRegistry.java
index d665fac555..867e7d2edf 100644
--- 
a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/util/PluginRegistry.java
+++ 
b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/util/PluginRegistry.java
@@ -20,6 +20,7 @@ package org.apache.logging.log4j.plugins.util;
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.plugins.Plugin;
 import org.apache.logging.log4j.plugins.PluginAliases;
+import org.apache.logging.log4j.plugins.Singleton;
 import org.apache.logging.log4j.plugins.processor.PluginCache;
 import org.apache.logging.log4j.plugins.processor.PluginEntry;
 import org.apache.logging.log4j.plugins.processor.PluginService;
@@ -41,13 +42,13 @@ import java.util.Map;
 import java.util.ServiceLoader;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicInteger;
-import java.util.function.Supplier;
 
 import static org.apache.logging.log4j.util.Unbox.box;
 
 /**
  * Registry singleton for PluginType maps partitioned by source type and then 
by category names.
  */
+@Singleton
 public class PluginRegistry {
 
     /**
@@ -57,8 +58,6 @@ public class PluginRegistry {
             
"META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat";
     private static final Logger LOGGER = StatusLogger.getLogger();
 
-    private static final Supplier<PluginRegistry> INSTANCE = new 
LazyValue<>(PluginRegistry::new);
-
     /**
      * Contains plugins found from {@link PluginService} services and legacy 
Log4j2Plugins.dat cache files in the main CLASSPATH.
      */
@@ -99,19 +98,6 @@ public class PluginRegistry {
      */
     private final Map<String, Categories> pluginCategoriesByPackage = new 
ConcurrentHashMap<>();
 
-    private PluginRegistry() {
-    }
-
-    /**
-     * Returns the global PluginRegistry instance.
-     *
-     * @return the global PluginRegistry instance.
-     * @since 2.1
-     */
-    public static PluginRegistry getInstance() {
-        return INSTANCE.get();
-    }
-
     /**
      * Resets the registry to an empty state.
      */
diff --git a/src/site/asciidoc/manual/dependencyinjection.adoc 
b/src/site/asciidoc/manual/dependencyinjection.adoc
index 272ba4b461..2f292c53c1 100644
--- a/src/site/asciidoc/manual/dependencyinjection.adoc
+++ b/src/site/asciidoc/manual/dependencyinjection.adoc
@@ -141,6 +141,7 @@ Some of these bindings were previously configured through 
various system propert
 
 * `org.apache.logging.log4j.core.ContextDataInjector`
 * `org.apache.logging.log4j.core.config.ConfigurationFactory`
+* `org.apache.logging.log4j.core.config.composite.MergeStrategy`
 * `org.apache.logging.log4j.core.impl.LogEventFactory`
 * `org.apache.logging.log4j.core.lookup.InterpolatorFactory`
 * `org.apache.logging.log4j.core.lookup.StrSubstitutor`
diff --git a/src/site/asciidoc/manual/plugins.adoc 
b/src/site/asciidoc/manual/plugins.adoc
index f198c86934..af3271bd0e 100644
--- a/src/site/asciidoc/manual/plugins.adoc
+++ b/src/site/asciidoc/manual/plugins.adoc
@@ -24,11 +24,11 @@ extend the PatternLayout class and add them via code. One 
goal of Log4j
 2 is to make extending it extremely easy through the use of plugins.
 
 In Log4j 2 a plugin is declared by adding a
-link:../log4j-core/apidocs/org/apache/logging/log4j/core/config/plugins/Plugin.html[`@Plugin`]
+link:../log4j-plugins/apidocs/org/apache/logging/log4j/plugins/Plugin.html[`@Plugin`]
 annotation to the class declaration. During initialization the
 
link:../log4j-core/apidocs/org/apache/logging/log4j/core/config/Configuration.html[`Configuration`]
 will invoke the
-link:../log4j-core/apidocs/org/apache/logging/log4j/core/config/plugins/util/PluginManager.html[`PluginManager`]
+link:../log4j-plugins/apidocs/org/apache/logging/log4j/plugins/util/PluginRegistry.html[`PluginRegistry`]
 to load the built-in Log4j plugins as well as any custom plugins. The
 `Injector` locates plugins by looking in five places:
 

Reply via email to