Index: src/site/xdoc/manual/plugins.xml =================================================================== --- src/site/xdoc/manual/plugins.xml (revision 1617189) +++ src/site/xdoc/manual/plugins.xml (working copy) @@ -38,15 +38,26 @@

In Log4j 2 a plugin is declared by adding a @Plugin 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:

+ If multiple Plugins specify the same (case-insensitive) name, then the load order above + determines which one will be used. For example, to override the File plugin which is provided + by the built-in FileAppender class, you would need to place your plugin in a JAR file in the + CLASSPATH ahead oflog4j-core.jar. This is not recommended; plugin name collisions will cause a + warning to be emitted. +

+

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 @@ -132,6 +143,13 @@ 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.

+

+ If multiple Converters specify the same ConverterKeys, then the load order above determines + which one will be used. For example, to override the %date converter which is provided by the + built-in DatePatternConverter class, you would need to place your plugin in a JAR file in the + CLASSPATH ahead oflog4j-core.jar. This is not recommended; pattern ConverterKeys collisions + will cause a warning to be emitted. Try to use unique ConverterKeys for your custom pattern converters. +

Index: log4j-core/src/test/java/org/apache/logging/log4j/core/config/plugins/processor/PluginProcessorTest.java =================================================================== --- log4j-core/src/test/java/org/apache/logging/log4j/core/config/plugins/processor/PluginProcessorTest.java (revision 1617189) +++ log4j-core/src/test/java/org/apache/logging/log4j/core/config/plugins/processor/PluginProcessorTest.java (working copy) @@ -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 testCategory = pluginCache.getCategory(p.category()); + final Map 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 =================================================================== --- log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/PluginCache.java (revision 1617189) +++ log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/PluginCache.java (working copy) @@ -25,25 +25,33 @@ 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; - /** * */ public class PluginCache { - private final transient PluginRegistry pluginCategories = new PluginRegistry(); + private final Map> categories = + new LinkedHashMap>(); + public Map> 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 getCategory(final String category) { - return pluginCategories.getCategory(category); + public Map getCategory(final String category) { + final String key = category.toLowerCase(); + Map result = categories.get(key); + if (result == null) { + categories.put(key, result = new LinkedHashMap()); + } + return result; } /** @@ -55,8 +63,10 @@ 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> 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> category : categories.entrySet()) { out.writeUTF(category.getKey()); final Map m = category.getValue(); out.writeInt(m.size()); @@ -81,7 +91,7 @@ * @throws IOException */ public void loadCacheFiles(final Enumeration 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 +99,7 @@ final int count = in.readInt(); for (int i = 0; i < count; i++) { final String category = in.readUTF(); - final ConcurrentMap m = pluginCategories.getCategory(category); + final Map m = getCategory(category); final int entries = in.readInt(); for (int j = 0; j < entries; j++) { final PluginEntry entry = new PluginEntry(); @@ -99,7 +109,9 @@ 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 { @@ -114,6 +126,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/plugins/processor/PluginProcessor.java =================================================================== --- log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/PluginProcessor.java (revision 1617189) +++ log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/PluginProcessor.java (working copy) @@ -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 category = pluginCache.getCategory(entry.getCategory()); + final Map category = pluginCache.getCategory(entry.getCategory()); category.put(entry.getKey(), entry); final Collection entries = element.accept(pluginAliasesVisitor, plugin); for (final PluginEntry pluginEntry : entries) { Index: log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/osgi/Activator.java =================================================================== --- log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/osgi/Activator.java (revision 1617189) +++ log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/osgi/Activator.java (working copy) @@ -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.INSTANCE.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.INSTANCE.clearBundlePlugins(bundle.getBundleId()); + } } @Override Index: log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/util/PluginType.java =================================================================== --- log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/util/PluginType.java (revision 1617189) +++ log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/util/PluginType.java (working copy) @@ -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 The plug-in class, which can be any kind of class. * @see org.apache.logging.log4j.core.config.plugins.Plugin */ -public class PluginType implements Serializable { +public class PluginType { - private static final long serialVersionUID = 4743255148794846612L; - + private final PluginEntry pluginEntry; private final Class pluginClass; private final String elementName; - private final boolean printObject; - private final boolean deferChildren; - public PluginType(final Class 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 pluginClass, final String elementName) { + this.pluginEntry = pluginEntry; + this.pluginClass = pluginClass; + this.elementName = elementName; } public Class 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: log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/util/PluginRegistry.java =================================================================== --- log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/util/PluginRegistry.java (revision 1617189) +++ log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/util/PluginRegistry.java (working copy) @@ -17,86 +17,248 @@ package org.apache.logging.log4j.core.config.plugins.util; -import java.io.Serializable; -import java.util.Map; -import java.util.Set; +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 java.io.*; +import java.net.URI; +import java.net.URL; +import java.text.DecimalFormat; +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; /** - * Registry for PluginType maps partitioned by category names. - * - * @param 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 { - private final ConcurrentMap> categories = - new ConcurrentHashMap>(); +public class PluginRegistry { + public static final PluginRegistry INSTANCE = new PluginRegistry(); + private final Logger LOGGER = StatusLogger.getLogger(); + + /** Contains plugins found in Log4j2Plugins.dat cache files in the main CLASSPATH. */ + private final AtomicReference>>> pluginsByCategoryRef = + new AtomicReference>>>(); + + /** Contains plugins found in Log4j2Plugins.dat cache files in OSGi Bundles. */ + private final ConcurrentMap>>> pluginsByCategoryByBundleId = + new ConcurrentHashMap>>>(); + + /** Contains plugins found by searching for annotated classes at runtime. */ + private final ConcurrentMap>>> pluginsByCategoryByPackage = + new ConcurrentHashMap>>>(); + + private PluginRegistry() + { + } + /** - * 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} + * Resets the registry to an empty state. */ - public ConcurrentMap getCategory(final String category) { - if (category == null) { - throw new IllegalArgumentException("Category name cannot be null."); + public void clear() { + pluginsByCategoryRef.set(null); + pluginsByCategoryByPackage.clear(); + pluginsByCategoryByBundleId.clear(); + } + + public Map>>> getPluginsByCategoryByBundleId() + { + return pluginsByCategoryByBundleId; + } + + @SuppressWarnings("unchecked") + public Map>> loadFromMainClassLoader() { + final Map>> existing = pluginsByCategoryRef.get(); + if (existing != null) { + // already loaded + return existing; } - final String key = category.toLowerCase(); - categories.putIfAbsent(key, new ConcurrentHashMap()); - return categories.get(key); + final ResourceLoader loader = new ClassLoaderResourceLoader(Loader.getClassLoader()); + final Map>> 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(); } - /** - * Returns the number of plugin categories currently available. This is primarily useful for serialization. - * - * @return the number of plugin categories. - */ - public int getCategoryCount() { - return categories.size(); + public void clearBundlePlugins(final long bundleId) + { + pluginsByCategoryByBundleId.remove(bundleId); } - /** - * 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. - */ - public boolean isEmpty() { - return categories.isEmpty(); + @SuppressWarnings("unchecked") + public Map>> loadFromBundle(final long bundleId, final ResourceLoader loader) { + Map>> existing = pluginsByCategoryByBundleId.get(bundleId); + if (existing != null) { + // already loaded from this classloader + return existing; + } + final Map>> 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; } - /** - * Resets the registry to an empty state. - */ - public void clear() { - categories.clear(); + @SuppressWarnings({"unchecked", "rawtypes"}) + private Map>> decodeCacheFiles(final ResourceLoader loader) { + final long startTime = System.nanoTime(); + final PluginCache cache = new PluginCache(); + try { + final Enumeration 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>> newPluginsByCategory = new HashMap>>(); + int pluginCount = 0; + for (final Map.Entry> outer : cache.getAllCategories().entrySet()) { + final String categoryLowerCase = outer.getKey(); + final List> types = new ArrayList>(outer.getValue().size()); + newPluginsByCategory.put(categoryLowerCase, types); + for (final Map.Entry 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; } - /** - * 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."); + @SuppressWarnings({"unchecked", "rawtypes"}) + public Map>> loadFromPackage(final String pkg) { + if (pkg == null || pkg.trim().length() == 0) { + // happens when splitting an empty string + return Collections.emptyMap(); } - final String key = category.toLowerCase(); - return categories.containsKey(key) && !categories.get(key).isEmpty(); + Map>> 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>> newPluginsByCategory = new HashMap>>(); + for (final Class clazz : resolver.getClasses()) { + final Plugin plugin = clazz.getAnnotation(Plugin.class); + final String categoryLowerCase = plugin.category().toLowerCase(); + List> list = newPluginsByCategory.get(categoryLowerCase); + if (list == null) { + newPluginsByCategory.put(categoryLowerCase, list = new ArrayList>()); + } + 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>> 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/util/PluginManager.java =================================================================== --- log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/util/PluginManager.java (revision 1617189) +++ log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/util/PluginManager.java (working copy) @@ -16,43 +16,34 @@ */ 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.Collection; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ConcurrentMap; +import java.util.*; 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.*; import org.apache.logging.log4j.status.StatusLogger; +import org.apache.logging.log4j.util.PropertiesUtil; /** * Loads and manages all the plugins. */ public class PluginManager { - // TODO: re-use PluginCache code from plugin processor - private static final PluginRegistry> REGISTRY = new PluginRegistry>(); + private static final String LOG4J_PACKAGE = "org.apache.logging.log4j.core"; + + public static final String PACKAGES_PROPERTY_NAME = "log4j.plugin.packages"; private static final CopyOnWriteArrayList PACKAGES = new CopyOnWriteArrayList(); - private static final String LOG4J_PACKAGES = "org.apache.logging.log4j.core"; private static final Logger LOGGER = StatusLogger.getLogger(); - private Map> plugins = new HashMap>(); + static { + final String value = PropertiesUtil.getProperties().getStringProperty(PACKAGES_PROPERTY_NAME, ""); + if (value.length() != 0) { + addPackages(Arrays.asList(value.split(Patterns.COMMA_SEPARATOR))); + } + } + + private volatile Map> plugins = Collections.emptyMap(); private final String category; /** @@ -90,10 +81,7 @@ 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); } /** @@ -102,10 +90,13 @@ * @param packages collection of package names to add. Ignored if {@code null} or empty. */ public static void addPackages(final Collection packages) { - if (packages == null || packages.isEmpty()) { - return; + if (packages != null) { + for (String pkg : packages) { + if (pkg.length() != 0) { + PACKAGES.addIfAbsent(pkg); + } + } } - PACKAGES.addAllAbsent(packages); } /** @@ -131,196 +122,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; + public void collectPlugins(List packages) { + final String categoryLowerCase = category.toLowerCase(); + final Map> newPlugins = new LinkedHashMap>(); + + // First, iterate the Log4j2Plugin.dat files found in the main CLASSPATH + Map>> builtInPlugins = PluginRegistry.INSTANCE.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.INSTANCE.loadFromPackage(LOG4J_PACKAGE); } - long start = System.nanoTime(); - if (preLoad) { - final ResourceLoader loader = new ClassLoaderResourceLoader(Loader.getClassLoader()); - loadPlugins(loader); - } - plugins = REGISTRY.getCategory(category); - loadFromPackages(start, preLoad); + mergeByName(newPlugins, builtInPlugins.get(categoryLowerCase)); - long elapsed = System.nanoTime() - start; - reportPluginLoadDuration(preLoad, elapsed); - } - - @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>> pluginsByCategory : PluginRegistry.INSTANCE.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.INSTANCE.loadFromPackage(pkg).get(categoryLowerCase)); } - for (final Class clazz : resolver.getClasses()) { - final Plugin plugin = clazz.getAnnotation(Plugin.class); - final String pluginCategory = plugin.category(); - final Map> 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.INSTANCE.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> registry = decode(loader); - if (registry != null) { - for (final Map.Entry>> entry : registry.getCategories()) { - REGISTRY.getCategory(entry.getKey()).putAll(entry.getValue()); - } - } else { - LOGGER.info("Plugin preloads not available from class loader {}", loader); - } + plugins = newPlugins; } - private static PluginRegistry> decode(final ResourceLoader loader) { - final Enumeration resources; - try { - resources = loader.getResources(PluginProcessor.PLUGIN_CACHE_FILE); - if (resources == null) { - return null; - } - } catch (final IOException ioe) { - LOGGER.warn("Unable to preload plugins", ioe); - return null; + private static void mergeByName(Map> newPlugins, List> plugins) { + if (plugins == null) { + return; } - final PluginRegistry> map = new PluginRegistry>(); - 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; + 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 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> 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 =================================================================== --- log4j-core/src/main/java/org/apache/logging/log4j/core/config/Configuration.java (revision 1617189) +++ log4j-core/src/main/java/org/apache/logging/log4j/core/config/Configuration.java (working copy) @@ -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; @@ -75,6 +76,8 @@ void removeLogger(final String name); + List getPluginPackages(); + Map getProperties(); void addListener(ConfigurationListener listener); Index: log4j-core/src/main/java/org/apache/logging/log4j/core/config/PropertiesPlugin.java =================================================================== --- log4j-core/src/main/java/org/apache/logging/log4j/core/config/PropertiesPlugin.java (revision 1617189) +++ log4j-core/src/main/java/org/apache/logging/log4j/core/config/PropertiesPlugin.java (working copy) @@ -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/config/xml/XmlConfiguration.java =================================================================== --- log4j-core/src/main/java/org/apache/logging/log4j/core/config/xml/XmlConfiguration.java (revision 1617189) +++ log4j-core/src/main/java/org/apache/logging/log4j/core/config/xml/XmlConfiguration.java (working copy) @@ -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/json/JsonConfiguration.java =================================================================== --- log4j-core/src/main/java/org/apache/logging/log4j/core/config/json/JsonConfiguration.java (revision 1617189) +++ log4j-core/src/main/java/org/apache/logging/log4j/core/config/json/JsonConfiguration.java (working copy) @@ -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/AbstractConfiguration.java =================================================================== --- log4j-core/src/main/java/org/apache/logging/log4j/core/config/AbstractConfiguration.java (revision 1617189) +++ log4j-core/src/main/java/org/apache/logging/log4j/core/config/AbstractConfiguration.java (working copy) @@ -20,12 +20,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.Serializable; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; @@ -100,6 +95,7 @@ private final StrSubstitutor subst = new StrSubstitutor(tempLookup); private LoggerConfig root = new LoggerConfig(); private final ConcurrentMap componentMap = new ConcurrentHashMap(); + protected final List pluginPackages = new ArrayList(); protected PluginManager pluginManager; private final ConfigurationSource configurationSource; @@ -119,6 +115,11 @@ } @Override + public List getPluginPackages() { + return pluginPackages; + } + + @Override public Map getProperties() { return properties; } @@ -130,9 +131,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> plugins = levelPlugins.getPlugins(); if (plugins != null) { for (final PluginType type : plugins.values()) { @@ -334,7 +335,7 @@ } else { final Map map = (Map) 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/lookup/Interpolator.java =================================================================== --- log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/Interpolator.java (revision 1617189) +++ log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/Interpolator.java (working copy) @@ -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 pluginPackages) { this.defaultLookup = defaultLookup == null ? new MapLookup(new HashMap()) : defaultLookup; final PluginManager manager = new PluginManager("Lookup"); - manager.collectPlugins(); + manager.collectPlugins(pluginPackages); final Map> plugins = manager.getPlugins(); for (final Map.Entry> entry : plugins.entrySet()) { Index: log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/PatternParser.java =================================================================== --- log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/PatternParser.java (revision 1617189) +++ log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/PatternParser.java (working copy) @@ -18,11 +18,7 @@ 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.List; -import java.util.Map; +import java.util.*; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.config.Configuration; @@ -126,9 +122,9 @@ final Class filterClass) { this.config = config; final PluginManager manager = new PluginManager(converterKey); - manager.collectPlugins(); + manager.collectPlugins(config == null ? null : config.getPluginPackages()); final Map> plugins = manager.getPlugins(); - final Map> converters = new HashMap>(); + final Map> converters = new LinkedHashMap>(); for (final PluginType type : plugins.values()) { try { @@ -140,7 +136,14 @@ final ConverterKeys keys = clazz.getAnnotation(ConverterKeys.class); if (keys != null) { for (final String key : keys.value()) { - converters.put(key, clazz); + final Class existing = converters.get(key); + if (existing == null) { + converters.put(key, clazz); + } + else if (!existing.equals(clazz)) { + LOGGER.warn("Converter key '{}' is mapped to {}, plugin {} will be ignored", + key, existing, clazz); + } } } } catch (final Exception ex) {