Index: log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/util/PluginManager.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/util/PluginManager.java	(date 1407708509000)
+++ log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/util/PluginManager.java	(revision )
@@ -16,43 +16,43 @@
  */
 package org.apache.logging.log4j.core.config.plugins.util;
 
-import java.io.BufferedInputStream;
-import java.io.DataInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URI;
-import java.net.URL;
-import java.text.DecimalFormat;
+import java.util.Arrays;
 import java.util.Collection;
-import java.util.Enumeration;
-import java.util.HashMap;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
-import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.CopyOnWriteArrayList;
 
 import org.apache.logging.log4j.Logger;
-import org.apache.logging.log4j.core.config.plugins.Plugin;
-import org.apache.logging.log4j.core.config.plugins.PluginAliases;
-import org.apache.logging.log4j.core.config.plugins.processor.PluginProcessor;
-import org.apache.logging.log4j.core.util.ClassLoaderResourceLoader;
-import org.apache.logging.log4j.core.util.Closer;
-import org.apache.logging.log4j.core.util.Loader;
-import org.apache.logging.log4j.core.util.ResourceLoader;
+import org.apache.logging.log4j.core.util.Patterns;
 import org.apache.logging.log4j.status.StatusLogger;
+import org.apache.logging.log4j.util.PropertiesUtil;
+import org.apache.logging.log4j.util.Strings;
 
 /**
  * Loads and manages all the plugins.
  */
 public class PluginManager {
 
-    // TODO: re-use PluginCache code from plugin processor
-    private static final PluginRegistry<PluginType<?>> REGISTRY = new PluginRegistry<PluginType<?>>();
+    private static final String LOG4J_PACKAGE = "org.apache.logging.log4j.core";
+
+    /**
+     * Name of the system property to set to define globally-used plugin packages where plugins may be found.
+     */
+    public static final String PACKAGES_PROPERTY_NAME = "log4j.plugin.packages";
     private static final CopyOnWriteArrayList<String> PACKAGES = new CopyOnWriteArrayList<String>();
-    private static final String LOG4J_PACKAGES = "org.apache.logging.log4j.core";
 
     private static final Logger LOGGER = StatusLogger.getLogger();
 
-    private Map<String, PluginType<?>> plugins = new HashMap<String, PluginType<?>>();
+    static {
+        final String value = PropertiesUtil.getProperties().getStringProperty(PACKAGES_PROPERTY_NAME, "");
+        if (Strings.isNotEmpty(value)) {
+            addPackages(Arrays.asList(value.split(Patterns.COMMA_SEPARATOR)));
+        }
+    }
+
+    private volatile Map<String, PluginType<?>> plugins = Collections.emptyMap();
     private final String category;
 
     /**
@@ -90,11 +90,8 @@
         if (p == null || p.isEmpty()) {
             return;
         }
-        if (PACKAGES.addIfAbsent(p)) {
-            // set of available plugins could have changed, reset plugin cache for newly-retrieved managers
-            REGISTRY.clear(); // TODO confirm if this is correct
+        PACKAGES.addIfAbsent(p);
-        }
+    }
-    }
 
     /**
      * Adds a list of package names to be scanned for plugins. Convenience method for {@link #addPackage(String)}.
@@ -102,11 +99,14 @@
      * @param packages collection of package names to add. Ignored if {@code null} or empty.
      */
     public static void addPackages(final Collection<String> packages) {
-        if (packages == null || packages.isEmpty()) {
-            return;
+        if (packages != null) {
+            for (String pkg : packages) {
+                if (pkg.length() != 0) {
+                    PACKAGES.addIfAbsent(pkg);
-        }
+                }
-        PACKAGES.addAllAbsent(packages);
-    }
+            }
+        }
+    }
 
     /**
      * Returns the type of a specified plugin.
@@ -131,196 +131,59 @@
      * Locates all the plugins.
      */
     public void collectPlugins() {
-        collectPlugins(true);
+        collectPlugins(null);
     }
 
     /**
-     * Collects plugins, optionally obtaining them from a preload map.
-     * 
-     * @param preLoad if true, plugins will be obtained from the preload map.
-     *
+     * Locates all the plugins including search of specific packages. Warns about name collisions.
      */
-    public void collectPlugins(boolean preLoad) {
-        if (REGISTRY.hasCategory(category)) {
-            plugins = REGISTRY.getCategory(category);
-            preLoad = false;
-        }
-        long start = System.nanoTime();
-        if (preLoad) {
-            final ResourceLoader loader = new ClassLoaderResourceLoader(Loader.getClassLoader());
-            loadPlugins(loader);
-        }
-        plugins = REGISTRY.getCategory(category);
-        loadFromPackages(start, preLoad);
+    public void collectPlugins(List<String> packages) {
+        final String categoryLowerCase = category.toLowerCase();
+        final Map<String, PluginType<?>> newPlugins = new LinkedHashMap<String, PluginType<?>>();
 
-        long elapsed = System.nanoTime() - start;
-        reportPluginLoadDuration(preLoad, elapsed);
+        // First, iterate the Log4j2Plugin.dat files found in the main CLASSPATH
+        Map<String, List<PluginType<?>>> builtInPlugins = PluginRegistry.getInstance().loadFromMainClassLoader();
+        if (builtInPlugins.isEmpty()) {
+            // If we didn't find any plugins above, someone must have messed with the log4j-core.jar.
+            // Search the standard package in the hopes we can find our core plugins.
+            builtInPlugins = PluginRegistry.getInstance().loadFromPackage(LOG4J_PACKAGE);
-    }
+        }
+        mergeByName(newPlugins, builtInPlugins.get(categoryLowerCase));
-    
+
-    @SuppressWarnings({ "unchecked", "rawtypes" })
-    private void loadFromPackages(final long start, final boolean preLoad) {
-        if (plugins == null || plugins.size() == 0) {
-            if (!PACKAGES.contains(LOG4J_PACKAGES)) {
-                PACKAGES.add(LOG4J_PACKAGES);
+        // Next, iterate any Log4j2Plugin.dat files from OSGi Bundles
+        for (final Map<String, List<PluginType<?>>> pluginsByCategory : PluginRegistry.getInstance().getPluginsByCategoryByBundleId().values()) {
+            mergeByName(newPlugins, pluginsByCategory.get(categoryLowerCase));
-            }
+        }
-        }
-        final ResolverUtil resolver = new ResolverUtil();
-        final ClassLoader classLoader = Loader.getClassLoader();
-        if (classLoader != null) {
-            resolver.setClassLoader(classLoader);
-        }
-        final Class<?> cls = null;
-        final ResolverUtil.Test test = new PluginTest(cls);
+
+        // Next iterate any packages passed to the static addPackage method.
         for (final String pkg : PACKAGES) {
-            resolver.findInPackage(test, pkg);
+            mergeByName(newPlugins, PluginRegistry.getInstance().loadFromPackage(pkg).get(categoryLowerCase));
         }
-        for (final Class<?> clazz : resolver.getClasses()) {
-            final Plugin plugin = clazz.getAnnotation(Plugin.class);
-            final String pluginCategory = plugin.category();
-            final Map<String, PluginType<?>> map = REGISTRY.getCategory(pluginCategory);
-            String type = plugin.elementType().equals(Plugin.EMPTY) ? plugin.name() : plugin.elementType();
-            PluginType<?> pluginType = new PluginType(clazz, type, plugin.printObject(), plugin.deferChildren());
-            map.put(plugin.name().toLowerCase(), pluginType);
-            final PluginAliases pluginAliases = clazz.getAnnotation(PluginAliases.class);
-            if (pluginAliases != null) {
-                for (String alias : pluginAliases.value()) {
-                    type =  plugin.elementType().equals(Plugin.EMPTY) ? alias : plugin.elementType();
-                    pluginType = new PluginType(clazz, type, plugin.printObject(), plugin.deferChildren());
-                    map.put(alias.trim().toLowerCase(), pluginType);
+        // Finally iterate any packages provided in the configuration (note these can be changed at runtime).
+        if (packages != null) {
+            for (final String pkg : packages) {
+                mergeByName(newPlugins, PluginRegistry.getInstance().loadFromPackage(pkg).get(categoryLowerCase));
-                }
-            }
+            }
+        }
-        }
-        plugins = REGISTRY.getCategory(category);
-    }
 
-    private void reportPluginLoadDuration(final boolean preLoad, long elapsed) {
-        final StringBuilder sb = new StringBuilder("Generated plugins in ");
-        DecimalFormat numFormat = new DecimalFormat("#0.000000");
-        final double seconds = elapsed / (1000.0 * 1000.0 * 1000.0);
-        sb.append(numFormat.format(seconds)).append(" seconds, packages: ");
-        sb.append(PACKAGES);
-        sb.append(", preload: ");
-        sb.append(preLoad);
-        sb.append(".");
-        LOGGER.debug(sb.toString());
-    }
+        LOGGER.debug("PluginManager '{}' found {} plugins", category, newPlugins.size());
 
-    public static void loadPlugins(final ResourceLoader loader) {
-        final PluginRegistry<PluginType<?>> registry = decode(loader);
-        if (registry != null) {
-            for (final Map.Entry<String, ConcurrentMap<String, PluginType<?>>> entry : registry.getCategories()) {
-                REGISTRY.getCategory(entry.getKey()).putAll(entry.getValue());
+        plugins = newPlugins;
-            }
+    }
-        } else {
-            LOGGER.info("Plugin preloads not available from class loader {}", loader);
-        }
-    }
 
-    private static PluginRegistry<PluginType<?>> decode(final ResourceLoader loader) {
-        final Enumeration<URL> resources;
-        try {
-            resources = loader.getResources(PluginProcessor.PLUGIN_CACHE_FILE);
-            if (resources == null) {
-                return null;
+    private static void mergeByName(Map<String, PluginType<?>> newPlugins, List<PluginType<?>> plugins) {
+        if (plugins == null) {
+            return;
-            }
+        }
-        } catch (final IOException ioe) {
-            LOGGER.warn("Unable to preload plugins", ioe);
-            return null;
+        for (final PluginType<?> pluginType : plugins) {
+            final String key = pluginType.getKey();
+            final PluginType<?> existing = newPlugins.get(key);
+            if (existing == null) {
+                newPlugins.put(key, pluginType);
+            } else if (!existing.getPluginClass().equals(pluginType.getPluginClass())) {
+                LOGGER.warn("Plugin [{}] is already mapped to {}, ignoring {}",
+                    key, existing.getPluginClass(), pluginType.getPluginClass());
-        }
+            }
-        final PluginRegistry<PluginType<?>> map = new PluginRegistry<PluginType<?>>();
-        while (resources.hasMoreElements()) {
-            final URL url = resources.nextElement();
-            LOGGER.debug("Found Plugin Map at {}", url.toExternalForm());
-            final InputStream is;
-            try {
-                is = url.openStream();
-            } catch (final IOException e) {
-                LOGGER.warn("Unable to open {}", url.toExternalForm(), e);
-                continue;
-            }
+        }
-            final DataInputStream dis = new DataInputStream(new BufferedInputStream(is));
-            try {
-                final int count = dis.readInt();
-                for (int j = 0; j < count; ++j) {
-                    final String category = dis.readUTF();
-                    final int entries = dis.readInt();
-                    final Map<String, PluginType<?>> types = map.getCategory(category);
-                    for (int i = 0; i < entries; ++i) {
-                        final String key = dis.readUTF();
-                        final String className = dis.readUTF();
-                        final String name = dis.readUTF();
-                        final boolean printable = dis.readBoolean();
-                        final boolean defer = dis.readBoolean();
-                        try {
-                            final Class<?> clazz = loader.loadClass(className);
-                            @SuppressWarnings({ "unchecked", "rawtypes" })
-                            final PluginType<?> pluginType = new PluginType(clazz, name, printable, defer);
-                            types.put(key, pluginType);
-                        } catch (final ClassNotFoundException e) {
-                            LOGGER.info("Plugin [{}] could not be loaded due to missing classes.", className, e);
-                        } catch (final VerifyError e) {
-                            LOGGER.info("Plugin [{}] could not be loaded due to verification error.", className, e);
-                        }
+    }
-                    }
-                }
-            } catch (final IOException ex) {
-                LOGGER.warn("Unable to preload plugins", ex);
-            } finally {
-                Closer.closeSilently(dis);
-            }
-        }
-        return map.isEmpty() ? null : map;
-    }
-
-    /**
-     * A Test that checks to see if each class is annotated with a specific annotation. If it
-     * is, then the test returns true, otherwise false.
-     */
-    public static class PluginTest implements ResolverUtil.Test {
-        private final Class<?> isA;
-
-        /**
-         * Constructs an AnnotatedWith test for the specified annotation type.
-         * @param isA The class to compare against.
-         */
-        public PluginTest(final Class<?> isA) {
-            this.isA = isA;
-        }
-
-        /**
-         * Returns true if the type is annotated with the class provided to the constructor.
-         * @param type The type to check for.
-         * @return true if the Class is of the specified type.
-         */
-        @Override
-        public boolean matches(final Class<?> type) {
-            return type != null && type.isAnnotationPresent(Plugin.class) &&
-                (isA == null || isA.isAssignableFrom(type));
-        }
-
-        @Override
-        public String toString() {
-            final StringBuilder msg = new StringBuilder("annotated with @" + Plugin.class.getSimpleName());
-            if (isA != null) {
-                msg.append(" is assignable to " + isA.getSimpleName());
-            }
-            return msg.toString();
-        }
-
-        @Override
-        public boolean matches(final URI resource) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public boolean doesMatchClass() {
-            return true;
-        }
-
-        @Override
-        public boolean doesMatchResource() {
-            return false;
-        }
-    }
-
 }
Index: log4j-core/src/main/java/org/apache/logging/log4j/core/config/Configuration.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- log4j-core/src/main/java/org/apache/logging/log4j/core/config/Configuration.java	(date 1407708509000)
+++ log4j-core/src/main/java/org/apache/logging/log4j/core/config/Configuration.java	(revision )
@@ -16,6 +16,7 @@
  */
 package org.apache.logging.log4j.core.config;
 
+import java.util.List;
 import java.util.Map;
 
 import org.apache.logging.log4j.core.Appender;
@@ -74,6 +75,8 @@
     void addLogger(final String name, final LoggerConfig loggerConfig);
 
     void removeLogger(final String name);
+
+    List<String> getPluginPackages();
 
     Map<String, String> getProperties();
 
Index: log4j-core/src/main/java/org/apache/logging/log4j/core/config/AbstractConfiguration.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- log4j-core/src/main/java/org/apache/logging/log4j/core/config/AbstractConfiguration.java	(date 1407708509000)
+++ log4j-core/src/main/java/org/apache/logging/log4j/core/config/AbstractConfiguration.java	(revision )
@@ -20,6 +20,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.Serializable;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
@@ -100,6 +101,7 @@
     private final StrSubstitutor subst = new StrSubstitutor(tempLookup);
     private LoggerConfig root = new LoggerConfig();
     private final ConcurrentMap<String, Object> componentMap = new ConcurrentHashMap<String, Object>();
+    protected final List<String> pluginPackages = new ArrayList<String>();
     protected PluginManager pluginManager;
     private final ConfigurationSource configurationSource;
 
@@ -119,6 +121,11 @@
     }
 
     @Override
+    public List<String> getPluginPackages() {
+        return pluginPackages;
+    }
+
+    @Override
     public Map<String, String> getProperties() {
         return properties;
     }
@@ -130,9 +137,9 @@
     public void start() {
         LOGGER.debug("Starting configuration {}", this);
         this.setStarting();
-        pluginManager.collectPlugins();
+        pluginManager.collectPlugins(pluginPackages);
         final PluginManager levelPlugins = new PluginManager("Level");
-        levelPlugins.collectPlugins();
+        levelPlugins.collectPlugins(pluginPackages);
         final Map<String, PluginType<?>> plugins = levelPlugins.getPlugins();
         if (plugins != null) {
             for (final PluginType<?> type : plugins.values()) {
@@ -334,7 +341,7 @@
         } else {
             final Map<String, String> map = (Map<String, String>) componentMap.get(CONTEXT_PROPERTIES);
             final StrLookup lookup = map == null ? null : new MapLookup(map);
-            subst.setVariableResolver(new Interpolator(lookup));
+            subst.setVariableResolver(new Interpolator(lookup, pluginPackages));
         }
 
         boolean setLoggers = false;
Index: log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/PatternParser.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/PatternParser.java	(date 1407708509000)
+++ log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/PatternParser.java	(revision )
@@ -19,8 +19,8 @@
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -126,9 +126,9 @@
             final Class<?> filterClass) {
         this.config = config;
         final PluginManager manager = new PluginManager(converterKey);
-        manager.collectPlugins();
+        manager.collectPlugins(config == null ? null : config.getPluginPackages());
         final Map<String, PluginType<?>> plugins = manager.getPlugins();
-        final Map<String, Class<PatternConverter>> converters = new HashMap<String, Class<PatternConverter>>();
+        final Map<String, Class<PatternConverter>> converters = new LinkedHashMap<String, Class<PatternConverter>>();
 
         for (final PluginType<?> type : plugins.values()) {
             try {
@@ -140,7 +140,13 @@
                 final ConverterKeys keys = clazz.getAnnotation(ConverterKeys.class);
                 if (keys != null) {
                     for (final String key : keys.value()) {
+                        if (converters.containsKey(key)) {
+                            LOGGER.warn("Converter key '{}' is already mapped to '{}'. " +
+                                    "Sorry, Dave, I can't let you do that! Ignoring plugin [{}].",
+                                key, converters.get(key), clazz);
+                        } else {
-                        converters.put(key, clazz);
+                            converters.put(key, clazz);
+                        }
                     }
                 }
             } catch (final Exception ex) {
Index: log4j-core/src/main/java/org/apache/logging/log4j/core/config/json/JsonConfiguration.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- log4j-core/src/main/java/org/apache/logging/log4j/core/config/json/JsonConfiguration.java	(date 1407708509000)
+++ log4j-core/src/main/java/org/apache/logging/log4j/core/config/json/JsonConfiguration.java	(revision )
@@ -32,7 +32,6 @@
 import org.apache.logging.log4j.core.config.FileConfigurationMonitor;
 import org.apache.logging.log4j.core.config.Node;
 import org.apache.logging.log4j.core.config.Reconfigurable;
-import org.apache.logging.log4j.core.config.plugins.util.PluginManager;
 import org.apache.logging.log4j.core.config.plugins.util.PluginType;
 import org.apache.logging.log4j.core.config.plugins.util.ResolverUtil;
 import org.apache.logging.log4j.core.config.status.StatusConfiguration;
@@ -85,7 +84,7 @@
                 } else if ("verbose".equalsIgnoreCase(entry.getKey())) {
                     statusConfig.withVerbosity(value);
                 } else if ("packages".equalsIgnoreCase(key)) {
-                    PluginManager.addPackages(Arrays.asList(value.split(Patterns.COMMA_SEPARATOR)));
+                    pluginPackages.addAll(Arrays.asList(value.split(Patterns.COMMA_SEPARATOR)));
                 } else if ("name".equalsIgnoreCase(key)) {
                     setName(value);
                 } else if ("monitorInterval".equalsIgnoreCase(key)) {
Index: log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/PluginProcessor.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/PluginProcessor.java	(date 1407708509000)
+++ log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/PluginProcessor.java	(revision )
@@ -22,8 +22,8 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Map;
 import java.util.Set;
-import java.util.concurrent.ConcurrentMap;
 
 import javax.annotation.processing.AbstractProcessor;
 import javax.annotation.processing.RoundEnvironment;
@@ -92,7 +92,7 @@
         for (final Element element : elements) {
             final Plugin plugin = element.getAnnotation(Plugin.class);
             final PluginEntry entry = element.accept(pluginVisitor, plugin);
-            final ConcurrentMap<String, PluginEntry> category = pluginCache.getCategory(entry.getCategory());
+            final Map<String, PluginEntry> category = pluginCache.getCategory(entry.getCategory());
             category.put(entry.getKey(), entry);
             final Collection<PluginEntry> entries = element.accept(pluginAliasesVisitor, plugin);
             for (final PluginEntry pluginEntry : entries) {
Index: log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/util/PluginRegistry.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/util/PluginRegistry.java	(date 1407725328000)
+++ log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/util/PluginRegistry.java	(revision )
@@ -17,86 +17,267 @@
 
 package org.apache.logging.log4j.core.config.plugins.util;
 
-import java.io.Serializable;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URL;
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicReference;
 
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginAliases;
+import org.apache.logging.log4j.core.config.plugins.processor.PluginCache;
+import org.apache.logging.log4j.core.config.plugins.processor.PluginEntry;
+import org.apache.logging.log4j.core.config.plugins.processor.PluginProcessor;
+import org.apache.logging.log4j.core.util.ClassLoaderResourceLoader;
+import org.apache.logging.log4j.core.util.Loader;
+import org.apache.logging.log4j.core.util.ResourceLoader;
+import org.apache.logging.log4j.status.StatusLogger;
+import org.apache.logging.log4j.util.Strings;
+
 /**
- * Registry for PluginType maps partitioned by category names.
- *
- * @param <T> plugin information object such as PluginType or PluginEntry.
+ * Registry singleton for PluginType maps partitioned by source type and then by category names.
  */
-public class PluginRegistry<T extends Serializable> {
-    private final ConcurrentMap<String, ConcurrentMap<String, T>> categories =
-        new ConcurrentHashMap<String, ConcurrentMap<String, T>>();
+public class PluginRegistry {
 
+    private final Logger LOGGER = StatusLogger.getLogger();
+
     /**
-     * Gets or creates a plugin category if not already available. Category names are case-insensitive. The
-     * ConcurrentMap that is returned should also be treated as a case-insensitive plugin map where names should be
-     * converted to lowercase before retrieval or storage.
-     *
-     * @param category the plugin category to look up or create.
-     * @return the plugin map for the given category name.
-     * @throws IllegalArgumentException if the argument is {@code null}
+     * Contains plugins found in Log4j2Plugins.dat cache files in the main CLASSPATH.
      */
-    public ConcurrentMap<String, T> getCategory(final String category) {
-        if (category == null) {
-            throw new IllegalArgumentException("Category name cannot be null.");
-        }
-        final String key = category.toLowerCase();
-        categories.putIfAbsent(key, new ConcurrentHashMap<String, T>());
-        return categories.get(key);
-    }
+    private final AtomicReference<Map<String, List<PluginType<?>>>> pluginsByCategoryRef =
+        new AtomicReference<Map<String, List<PluginType<?>>>>();
 
     /**
-     * Returns the number of plugin categories currently available. This is primarily useful for serialization.
-     *
-     * @return the number of plugin categories.
+     * Contains plugins found in Log4j2Plugins.dat cache files in OSGi Bundles.
      */
-    public int getCategoryCount() {
-        return categories.size();
-    }
+    private final ConcurrentMap<Long, Map<String, List<PluginType<?>>>> pluginsByCategoryByBundleId =
+        new ConcurrentHashMap<Long, Map<String, List<PluginType<?>>>>();
 
     /**
-     * Indicates whether or not any plugin categories have been registered. Note that this does not necessarily
-     * indicate if any plugins are registered as categories may be empty.
-     *
-     * @return {@code true} if there any categories registered.
+     * Contains plugins found by searching for annotated classes at runtime.
      */
-    public boolean isEmpty() {
-        return categories.isEmpty();
+    private final ConcurrentMap<String, Map<String, List<PluginType<?>>>> pluginsByCategoryByPackage =
+        new ConcurrentHashMap<String, Map<String, List<PluginType<?>>>>();
+
+    private PluginRegistry() {
     }
 
+    private static class Holder {
+        // the usual initialization-on-demand holder idiom
+        // https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom
+        private static final PluginRegistry INSTANCE = new PluginRegistry();
+    }
+
+    public static PluginRegistry getInstance() {
+        return Holder.INSTANCE;
+    }
+
     /**
      * Resets the registry to an empty state.
      */
     public void clear() {
-        categories.clear();
+        pluginsByCategoryRef.set(null);
+        pluginsByCategoryByPackage.clear();
+        pluginsByCategoryByBundleId.clear();
     }
 
-    /**
-     * Indicates whether or not the given category name is registered and has plugins in that category.
-     *
-     * @param category the plugin category name to check.
-     * @return {@code true} if the category exists and has plugins registered.
-     * @throws IllegalArgumentException if the argument is {@code null}
-     */
-    public boolean hasCategory(final String category) {
-        if (category == null) {
-            throw new IllegalArgumentException("Category name cannot be null.");
+    public Map<Long, Map<String, List<PluginType<?>>>> getPluginsByCategoryByBundleId() {
+        return pluginsByCategoryByBundleId;
-        }
+    }
-        final String key = category.toLowerCase();
-        return categories.containsKey(key) && !categories.get(key).isEmpty();
+
+    @SuppressWarnings("unchecked")
+    public Map<String, List<PluginType<?>>> loadFromMainClassLoader() {
+        final Map<String, List<PluginType<?>>> existing = pluginsByCategoryRef.get();
+        if (existing != null) {
+            // already loaded
+            return existing;
-    }
+        }
+        final ResourceLoader loader = new ClassLoaderResourceLoader(Loader.getClassLoader());
+        final Map<String, List<PluginType<?>>> newPluginsByCategory = decodeCacheFiles(loader);
 
+        // Note multiple threads could be calling this method concurrently. Both will do the work,
+        // but only one will be allowed to store the result in the AtomicReference.
+        // Return the map produced by whichever thread won the race, so all callers will get the same result.
+        if (pluginsByCategoryRef.compareAndSet(null, newPluginsByCategory)) {
+            return newPluginsByCategory;
+        }
+        return pluginsByCategoryRef.get();
+    }
+
+    public void clearBundlePlugins(final long bundleId) {
+        pluginsByCategoryByBundleId.remove(bundleId);
+    }
+
+    @SuppressWarnings("unchecked")
+    public Map<String, List<PluginType<?>>> loadFromBundle(final long bundleId, final ResourceLoader loader) {
+        Map<String, List<PluginType<?>>> existing = pluginsByCategoryByBundleId.get(bundleId);
+        if (existing != null) {
+            // already loaded from this classloader
+            return existing;
+        }
+        final Map<String, List<PluginType<?>>> newPluginsByCategory = decodeCacheFiles(loader);
+
+        // Note multiple threads could be calling this method concurrently. Both will do the work,
+        // but only one will be allowed to store the result in the outer map.
+        // Return the inner map produced by whichever thread won the race, so all callers will get the same result.
+        existing = pluginsByCategoryByBundleId.putIfAbsent(bundleId, newPluginsByCategory);
+        if (existing != null) {
+            return existing;
+        }
+        return newPluginsByCategory;
+    }
+
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    private Map<String, List<PluginType<?>>> decodeCacheFiles(final ResourceLoader loader) {
+        final long startTime = System.nanoTime();
+        final PluginCache cache = new PluginCache();
+        try {
+            final Enumeration<URL> resources = loader.getResources(PluginProcessor.PLUGIN_CACHE_FILE);
+            if (resources == null) {
+                LOGGER.info("Plugin preloads not available from class loader {}", loader);
+            } else {
+                cache.loadCacheFiles(resources);
+            }
+        } catch (final IOException ioe) {
+            LOGGER.warn("Unable to preload plugins", ioe);
+        }
+        final Map<String, List<PluginType<?>>> newPluginsByCategory = new HashMap<String, List<PluginType<?>>>();
+        int pluginCount = 0;
+        for (final Map.Entry<String, Map<String, PluginEntry>> outer : cache.getAllCategories().entrySet()) {
+            final String categoryLowerCase = outer.getKey();
+            final List<PluginType<?>> types = new ArrayList<PluginType<?>>(outer.getValue().size());
+            newPluginsByCategory.put(categoryLowerCase, types);
+            for (final Map.Entry<String, PluginEntry> inner : outer.getValue().entrySet()) {
+                final PluginEntry entry = inner.getValue();
+                final String className = entry.getClassName();
+                try {
+                    final Class<?> clazz = loader.loadClass(className);
+                    types.add(new PluginType(entry, clazz, entry.getName()));
+                    ++pluginCount;
+                } catch (final ClassNotFoundException e) {
+                    LOGGER.info("Plugin [{}] could not be loaded due to missing classes.", className, e);
+                } catch (final VerifyError e) {
+                    LOGGER.info("Plugin [{}] could not be loaded due to verification error.", className, e);
+                }
+            }
+        }
+
+        final long endTime = System.nanoTime();
+        final DecimalFormat numFormat = new DecimalFormat("#0.000000");
+        final double seconds = (endTime - startTime) * 1e-9;
+        LOGGER.debug("Took {} seconds to load {} plugins from {}",
+            numFormat.format(seconds), pluginCount, loader);
+        return newPluginsByCategory;
+    }
+
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    public Map<String, List<PluginType<?>>> loadFromPackage(final String pkg) {
+        if (Strings.isBlank(pkg)) {
+            // happens when splitting an empty string
+            return Collections.emptyMap();
+        }
+        Map<String, List<PluginType<?>>> existing = pluginsByCategoryByPackage.get(pkg);
+        if (existing != null) {
+            // already loaded this package
+            return existing;
+        }
+
+        final long startTime = System.nanoTime();
+        final ResolverUtil resolver = new ResolverUtil();
+        final ClassLoader classLoader = Loader.getClassLoader();
+        if (classLoader != null) {
+            resolver.setClassLoader(classLoader);
+        }
+        resolver.findInPackage(new PluginTest(), pkg);
+
+        final Map<String, List<PluginType<?>>> newPluginsByCategory = new HashMap<String, List<PluginType<?>>>();
+        for (final Class<?> clazz : resolver.getClasses()) {
+            final Plugin plugin = clazz.getAnnotation(Plugin.class);
+            final String categoryLowerCase = plugin.category().toLowerCase();
+            List<PluginType<?>> list = newPluginsByCategory.get(categoryLowerCase);
+            if (list == null) {
+                newPluginsByCategory.put(categoryLowerCase, list = new ArrayList<PluginType<?>>());
+            }
+            final PluginEntry mainEntry = new PluginEntry();
+            final String mainElementName = plugin.elementType().equals(
+                Plugin.EMPTY) ? plugin.name() : plugin.elementType();
+            mainEntry.setKey(plugin.name().toLowerCase());
+            mainEntry.setName(plugin.name());
+            mainEntry.setCategory(plugin.category());
+            mainEntry.setClassName(clazz.getName());
+            mainEntry.setPrintable(plugin.printObject());
+            mainEntry.setDefer(plugin.deferChildren());
+            list.add(new PluginType(mainEntry, clazz, mainElementName));
+            final PluginAliases pluginAliases = clazz.getAnnotation(PluginAliases.class);
+            if (pluginAliases != null) {
+                for (String alias : pluginAliases.value()) {
+                    final PluginEntry aliasEntry = new PluginEntry();
+                    final String aliasElementName = plugin.elementType().equals(
+                        Plugin.EMPTY) ? alias.trim() : plugin.elementType();
+                    aliasEntry.setKey(alias.trim().toLowerCase());
+                    aliasEntry.setName(plugin.name());
+                    aliasEntry.setCategory(plugin.category());
+                    aliasEntry.setClassName(clazz.getName());
+                    aliasEntry.setPrintable(plugin.printObject());
+                    aliasEntry.setDefer(plugin.deferChildren());
+                    list.add(new PluginType(aliasEntry, clazz, aliasElementName));
+                }
+            }
+        }
+
+        final long endTime = System.nanoTime();
+        final DecimalFormat numFormat = new DecimalFormat("#0.000000");
+        final double seconds = (endTime - startTime) * 1e-9;
+        LOGGER.debug("Took {} seconds to load {} plugins from package {}",
+            numFormat.format(seconds), resolver.getClasses().size(), pkg);
+
+        // Note multiple threads could be calling this method concurrently. Both will do the work,
+        // but only one will be allowed to store the result in the outer map.
+        // Return the inner map produced by whichever thread won the race, so all callers will get the same result.
+        existing = pluginsByCategoryByPackage.putIfAbsent(pkg, newPluginsByCategory);
+        if (existing != null) {
+            return existing;
+        }
+        return newPluginsByCategory;
+    }
+
     /**
-     * Gets an entry set for iterating over the registered plugin categories.
-     *
-     * @return an entry set of the registered plugin categories.
+     * A Test that checks to see if each class is annotated with the 'Plugin' annotation. If it
+     * is, then the test returns true, otherwise false.
      */
-    public Set<Map.Entry<String, ConcurrentMap<String, T>>> getCategories() {
-        return categories.entrySet();
+    public static class PluginTest implements ResolverUtil.Test {
+        @Override
+        public boolean matches(final Class<?> type) {
+            return type != null && type.isAnnotationPresent(Plugin.class);
+        }
+
+        @Override
+        public String toString() {
+            return "annotated with @" + Plugin.class.getSimpleName();
+        }
+
+        @Override
+        public boolean matches(final URI resource) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean doesMatchClass() {
+            return true;
+        }
+
+        @Override
+        public boolean doesMatchResource() {
+            return false;
+        }
     }
 }
Index: log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/osgi/Activator.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/osgi/Activator.java	(date 1407708509000)
+++ log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/osgi/Activator.java	(revision )
@@ -20,7 +20,7 @@
 import java.util.concurrent.atomic.AtomicReference;
 
 import org.apache.logging.log4j.Logger;
-import org.apache.logging.log4j.core.config.plugins.util.PluginManager;
+import org.apache.logging.log4j.core.config.plugins.util.PluginRegistry;
 import org.apache.logging.log4j.core.util.BundleResourceLoader;
 import org.apache.logging.log4j.status.StatusLogger;
 import org.osgi.framework.Bundle;
@@ -59,13 +59,18 @@
 
     private static void scanBundleForPlugins(final Bundle bundle) {
         LOGGER.trace("Scanning bundle [{}] for plugins.", bundle.getSymbolicName());
-        PluginManager.loadPlugins(new BundleResourceLoader(bundle));
+        PluginRegistry.getInstance().loadFromBundle(bundle.getBundleId(), new BundleResourceLoader(bundle));
     }
 
     @Override
     public void stop(final BundleContext context) throws Exception {
         // not much can be done that isn't already automated by the framework
         this.context.compareAndSet(context, null);
+
+        // TODO not sure if we should uninstall Bundle plugins, and if so will this do the trick
+        for (final Bundle bundle : context.getBundles()) {
+            PluginRegistry.getInstance().clearBundlePlugins(bundle.getBundleId());
+        }
     }
 
     @Override
Index: log4j-core/src/test/java/org/apache/logging/log4j/core/config/plugins/processor/PluginProcessorTest.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- log4j-core/src/test/java/org/apache/logging/log4j/core/config/plugins/processor/PluginProcessorTest.java	(date 1407708509000)
+++ log4j-core/src/test/java/org/apache/logging/log4j/core/config/plugins/processor/PluginProcessorTest.java	(revision )
@@ -19,7 +19,7 @@
 
 import java.net.URL;
 import java.util.Enumeration;
-import java.util.concurrent.ConcurrentMap;
+import java.util.Map;
 
 import org.apache.logging.log4j.core.config.plugins.Plugin;
 import org.apache.logging.log4j.core.config.plugins.PluginAliases;
@@ -47,7 +47,7 @@
     @Test
     public void testTestCategoryFound() throws Exception {
         assertNotNull("No plugin annotation on FakePlugin.", p);
-        final ConcurrentMap<String, PluginEntry> testCategory = pluginCache.getCategory(p.category());
+        final Map<String, PluginEntry> testCategory = pluginCache.getCategory(p.category());
         assertNotEquals("No plugins were found.", 0, pluginCache.size());
         assertNotNull("The category '" + p.category() + "' was not found.", testCategory);
         assertFalse(testCategory.isEmpty());
Index: log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/PluginCache.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/PluginCache.java	(date 1407708509000)
+++ log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/PluginCache.java	(revision )
@@ -25,26 +25,35 @@
 import java.io.OutputStream;
 import java.net.URL;
 import java.util.Enumeration;
+import java.util.LinkedHashMap;
 import java.util.Map;
-import java.util.concurrent.ConcurrentMap;
 
-import org.apache.logging.log4j.core.config.plugins.util.PluginRegistry;
+import org.apache.logging.log4j.core.util.Closer;
 
 /**
  *
  */
 public class PluginCache {
-    private final transient PluginRegistry<PluginEntry> pluginCategories = new PluginRegistry<PluginEntry>();
+    private final Map<String, Map<String, PluginEntry>> categories =
+        new LinkedHashMap<String, Map<String, PluginEntry>>();
 
+    public Map<String, Map<String, PluginEntry>> getAllCategories() {
+        return categories;
+    }
+
     /**
      * Gets or creates a category of plugins.
      *
      * @param category name of category to look up.
      * @return plugin mapping of names to plugin entries.
      */
-    public ConcurrentMap<String, PluginEntry> getCategory(final String category) {
-        return pluginCategories.getCategory(category);
+    public Map<String, PluginEntry> getCategory(final String category) {
+        final String key = category.toLowerCase();
+        if (!categories.containsKey(key)) {
+            categories.put(key, new LinkedHashMap<String, PluginEntry>());
-    }
+        }
+        return categories.get(key);
+    }
 
     /**
      * Stores the plugin cache to a given OutputStream.
@@ -52,11 +61,14 @@
      * @param os destination to save cache to.
      * @throws IOException
      */
+    // NOTE: if this file format is to be changed, the filename should change and this format should still be readable
     public void writeCache(final OutputStream os) throws IOException {
         final DataOutputStream out = new DataOutputStream(new BufferedOutputStream(os));
         try {
-            out.writeInt(pluginCategories.getCategoryCount());
-            for (final Map.Entry<String, ConcurrentMap<String, PluginEntry>> category : pluginCategories.getCategories()) {
+            // See PluginManager.readFromCacheFiles for the corresponding decoder. Format may not be changed
+            // without breaking existing Log4j2Plugins.dat files.
+            out.writeInt(categories.size());
+            for (final Map.Entry<String, Map<String, PluginEntry>> category : categories.entrySet()) {
                 out.writeUTF(category.getKey());
                 final Map<String, PluginEntry> m = category.getValue();
                 out.writeInt(m.size());
@@ -70,7 +82,7 @@
                 }
             }
         } finally {
-            out.close();
+            Closer.closeSilently(out);
         }
     }
 
@@ -81,7 +93,7 @@
      * @throws IOException
      */
     public void loadCacheFiles(final Enumeration<URL> resources) throws IOException {
-        pluginCategories.clear();
+        categories.clear();
         while (resources.hasMoreElements()) {
             final URL url = resources.nextElement();
             final DataInputStream in = new DataInputStream(new BufferedInputStream(url.openStream()));
@@ -89,7 +101,7 @@
                 final int count = in.readInt();
                 for (int i = 0; i < count; i++) {
                     final String category = in.readUTF();
-                    final ConcurrentMap<String, PluginEntry> m = pluginCategories.getCategory(category);
+                    final Map<String, PluginEntry> m = getCategory(category);
                     final int entries = in.readInt();
                     for (int j = 0; j < entries; j++) {
                         final PluginEntry entry = new PluginEntry();
@@ -99,11 +111,13 @@
                         entry.setPrintable(in.readBoolean());
                         entry.setDefer(in.readBoolean());
                         entry.setCategory(category);
-                        m.putIfAbsent(entry.getKey(), entry);
+                        if (!m.containsKey(entry.getKey())) {
+                            m.put(entry.getKey(), entry);
-                    }
-                }
+                        }
+                    }
+                }
             } finally {
-                in.close();
+                Closer.closeSilently(in);
             }
         }
     }
@@ -114,6 +128,6 @@
      * @return number of plugin categories in cache.
      */
     public int size() {
-        return pluginCategories.getCategoryCount();
+        return categories.size();
     }
 }
Index: log4j-core/src/main/java/org/apache/logging/log4j/core/config/xml/XmlConfiguration.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- log4j-core/src/main/java/org/apache/logging/log4j/core/config/xml/XmlConfiguration.java	(date 1407708509000)
+++ log4j-core/src/main/java/org/apache/logging/log4j/core/config/xml/XmlConfiguration.java	(revision )
@@ -41,7 +41,6 @@
 import org.apache.logging.log4j.core.config.FileConfigurationMonitor;
 import org.apache.logging.log4j.core.config.Node;
 import org.apache.logging.log4j.core.config.Reconfigurable;
-import org.apache.logging.log4j.core.config.plugins.util.PluginManager;
 import org.apache.logging.log4j.core.config.plugins.util.PluginType;
 import org.apache.logging.log4j.core.config.plugins.util.ResolverUtil;
 import org.apache.logging.log4j.core.config.status.StatusConfiguration;
@@ -150,7 +149,7 @@
                 } else if ("verbose".equalsIgnoreCase(key)) {
                     statusConfig.withVerbosity(value);
                 } else if ("packages".equalsIgnoreCase(key)) {
-                    PluginManager.addPackages(Arrays.asList(value.split(Patterns.COMMA_SEPARATOR)));
+                    pluginPackages.addAll(Arrays.asList(value.split(Patterns.COMMA_SEPARATOR)));
                 } else if ("name".equalsIgnoreCase(key)) {
                     setName(value);
                 } else if ("strict".equalsIgnoreCase(key)) {
Index: log4j-core/src/main/java/org/apache/logging/log4j/core/config/PropertiesPlugin.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- log4j-core/src/main/java/org/apache/logging/log4j/core/config/PropertiesPlugin.java	(date 1407708509000)
+++ log4j-core/src/main/java/org/apache/logging/log4j/core/config/PropertiesPlugin.java	(revision )
@@ -54,6 +54,6 @@
             map.put(prop.getName(), prop.getValue());
         }
 
-        return new Interpolator(new MapLookup(map));
+        return new Interpolator(new MapLookup(map), config.getPluginPackages());
     }
 }
Index: log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/Interpolator.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/Interpolator.java	(date 1407708509000)
+++ log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/Interpolator.java	(revision )
@@ -17,6 +17,7 @@
 package org.apache.logging.log4j.core.lookup;
 
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import org.apache.logging.log4j.Logger;
@@ -41,9 +42,13 @@
     private final StrLookup defaultLookup;
 
     public Interpolator(final StrLookup defaultLookup) {
+        this(defaultLookup, null);
+    }
+
+    public Interpolator(final StrLookup defaultLookup, final List<String> pluginPackages) {
         this.defaultLookup = defaultLookup == null ? new MapLookup(new HashMap<String, String>()) : defaultLookup;
         final PluginManager manager = new PluginManager("Lookup");
-        manager.collectPlugins();
+        manager.collectPlugins(pluginPackages);
         final Map<String, PluginType<?>> plugins = manager.getPlugins();
 
         for (final Map.Entry<String, PluginType<?>> entry : plugins.entrySet()) {
Index: log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/util/PluginType.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/util/PluginType.java	(date 1407708509000)
+++ log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/util/PluginType.java	(revision )
@@ -17,7 +17,7 @@
 package org.apache.logging.log4j.core.config.plugins.util;
 
 
-import java.io.Serializable;
+import org.apache.logging.log4j.core.config.plugins.processor.PluginEntry;
 
 /**
  * Plugin Descriptor. This is a memento object for Plugin annotations paired to their annotated classes.
@@ -25,20 +25,16 @@
  * @param <T> The plug-in class, which can be any kind of class.
  * @see org.apache.logging.log4j.core.config.plugins.Plugin
  */
-public class PluginType<T> implements Serializable {
+public class PluginType<T> {
 
-    private static final long serialVersionUID = 4743255148794846612L;
-
+    private final PluginEntry pluginEntry;
     private final Class<T> pluginClass;
     private final String elementName;
-    private final boolean printObject;
-    private final boolean deferChildren;
 
-    public PluginType(final Class<T> clazz, final String name, final boolean printObj, final boolean deferChildren) {
-        this.pluginClass = clazz;
-        this.elementName = name;
-        this.printObject = printObj;
-        this.deferChildren = deferChildren;
+    public PluginType(final PluginEntry pluginEntry, final Class<T> pluginClass, final String elementName) {
+        this.pluginEntry = pluginEntry;
+        this.pluginClass = pluginClass;
+        this.elementName = elementName;
     }
 
     public Class<T> getPluginClass() {
@@ -49,17 +45,30 @@
         return this.elementName;
     }
 
+    public String getKey() {
+        return this.pluginEntry.getKey();
+    }
+
     public boolean isObjectPrintable() {
-        return this.printObject;
+        return this.pluginEntry.isPrintable();
     }
 
     public boolean isDeferChildren() {
-        return this.deferChildren;
+        return this.pluginEntry.isDefer();
     }
 
+    public String getCategory() {
+        return this.pluginEntry.getCategory();
+    }
+
     @Override
     public String toString() {
-        return "PluginType [pluginClass=" + this.pluginClass + ", elementName=" + this.elementName + ", printObject="
-                + this.printObject + ", deferChildren=" + this.deferChildren + "]";
+        return "PluginType [pluginClass=" + pluginClass +
+                ", key=" + pluginEntry.getKey() +
+                ", elementName=" + pluginEntry.getName() +
+                ", isObjectPrintable=" + pluginEntry.isPrintable() +
+                ", isDeferChildren==" + pluginEntry.isDefer() +
+                ", category=" + pluginEntry.getCategory() +
+                "]";
     }
 }
Index: src/site/xdoc/manual/plugins.xml
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- src/site/xdoc/manual/plugins.xml	(date 1407708509000)
+++ src/site/xdoc/manual/plugins.xml	(revision )
@@ -38,15 +38,26 @@
           <p>
             In Log4j 2 a plugin is declared by adding a <code>@Plugin</code> annotation to the class declaration. During
             initialization the Configuration will invoke the PluginManager to load the built-in Log4j plugins
-            as well as any custom plugins. The PluginManager locates plugins by looking in two places:
+            as well as any custom plugins. The PluginManager locates plugins by looking in four places:
           </p>
           <ul>
             <li>Serialized plugin listing files on the classpath. These files are generated automatically during
               the build (more details below).</li>
+            <li>A comma-separated list of packages specified by the <code>log4j.plugin.packages</code>
+              system property.</li>
+            <li>Packages passed to the static <code>PluginManager.addPackages</code> method (before Log4j configuration
+              occurs).</li>
             <li>The <a href="./configuration.html#ConfigurationSyntax">packages</a> declared in your log4j2
               configuration file.</li>
           </ul>
           <p>
+            If multiple Plugins specify the same (case-insensitive) <code>name</code>, then the load order above
+            determines which one will be used. For example, to override the <code>File</code> plugin which is provided
+            by the built-in <code>FileAppender</code> class, you would need to place your plugin in a JAR file in the
+            CLASSPATH ahead of<code>log4j-core.jar</code>. This is not recommended; plugin name collisions will cause a
+            warning to be emitted.
+          </p>
+          <p>
             Serialized plugin listing files are generated by an annotation processor contained in the
             log4j-core artifact which will automatically scan your code for Log4j 2 plugins and output a metadata
             file in your processed classes. There is nothing extra that needs to be done to enable this; the Java
@@ -131,6 +142,13 @@
             one that takes an array of Objects instead of the LogEvent. Both append to the provided StringBuilder
             in the same fashion as a LogEventPatternConverter. These Converters are typically used by the
             RollingFileAppender to construct the name of the file to log to.
+          </p>
+          <p>
+            If multiple Converters specify the same <code>ConverterKeys</code>, then the load order above determines
+            which one will be used. For example, to override the <code>%date</code> converter which is provided by the
+            built-in <code>DatePatternConverter</code> class, you would need to place your plugin in a JAR file in the
+            CLASSPATH ahead of<code>log4j-core.jar</code>. This is not recommended; pattern ConverterKeys collisions
+            will cause a warning to be emitted. Try to use unique ConverterKeys for your custom pattern converters.
           </p>
         </subsection>
         <subsection name="KeyProviders">
