    001    /*
    002     * Licensed to the Apache Software Foundation (ASF) under one or more
    003     * contributor license agreements.  See the NOTICE file distributed with
    004     * this work for additional information regarding copyright ownership.
    005     * The ASF licenses this file to You under the Apache License, Version 2.0
    006     * (the "License"); you may not use this file except in compliance with
    007     * the License.  You may obtain a copy of the License at
    008     *
    009     *     http://www.apache.org/licenses/LICENSE-2.0
    010     *
    011     * Unless required by applicable law or agreed to in writing, software
    012     * distributed under the License is distributed on an "AS IS" BASIS,
    013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014     * See the License for the specific language governing permissions and
    015     * limitations under the License.
    016     */
    017
    018    package org.apache.logging.log4j.core.config.plugins.util;
    019
    020    import java.io.IOException;
    021    import java.net.URI;
    022    import java.net.URL;
    023    import java.text.DecimalFormat;
    024    import java.util.ArrayList;
    025    import java.util.Collections;
    026    import java.util.Enumeration;
    027    import java.util.HashMap;
    028    import java.util.List;
    029    import java.util.Map;
    030    import java.util.concurrent.ConcurrentHashMap;
    031    import java.util.concurrent.ConcurrentMap;
    032    import java.util.concurrent.atomic.AtomicReference;
    033
    034    import org.apache.logging.log4j.Logger;
    035    import org.apache.logging.log4j.core.config.plugins.Plugin;
    036    import org.apache.logging.log4j.core.config.plugins.PluginAliases;
    037    import org.apache.logging.log4j.core.config.plugins.processor.PluginCache;
    038    import org.apache.logging.log4j.core.config.plugins.processor.PluginEntry;
    039    import org.apache.logging.log4j.core.config.plugins.processor.PluginProcessor;
    040    import org.apache.logging.log4j.core.util.Loader;
    041    import org.apache.logging.log4j.status.StatusLogger;
    042    import org.apache.logging.log4j.util.Strings;
    043
    044    /**
    045     * Registry singleton for PluginType maps partitioned by source type and then by category names.
    046     */
    047    public class PluginRegistry {
    048
    049        private static final Logger LOGGER = StatusLogger.getLogger();
    050
    051        private static volatile PluginRegistry INSTANCE;
    052        private static final Object INSTANCE_LOCK = new Object();
    053
    054        /**
    055         * Contains plugins found in Log4j2Plugins.dat cache files in the main CLASSPATH.
    056         */
    057        private final AtomicReference<Map<String, List<PluginType<?>>>> pluginsByCategoryRef =
    058            new AtomicReference<Map<String, List<PluginType<?>>>>();
    059
    060        /**
    061         * Contains plugins found in Log4j2Plugins.dat cache files in OSGi Bundles.
    062         */
    063        private final ConcurrentMap<Long, Map<String, List<PluginType<?>>>> pluginsByCategoryByBundleId =
    064            new ConcurrentHashMap<Long, Map<String, List<PluginType<?>>>>();
    065
    066        /**
    067         * Contains plugins found by searching for annotated classes at runtime.
    068         */
    069        private final ConcurrentMap<String, Map<String, List<PluginType<?>>>> pluginsByCategoryByPackage =
    070            new ConcurrentHashMap<String, Map<String, List<PluginType<?>>>>();
    071
    072        private PluginRegistry() {
    073        }
    074
    075        /**
    076         * Returns the global PluginRegistry instance.
    077         *
    078         * @return the global PluginRegistry instance.
    079         * @since 2.1
    080         */
    081        public static PluginRegistry getInstance() {
    082            PluginRegistry result = INSTANCE;
    083            if (result == null) {
    084                synchronized (INSTANCE_LOCK) {
    085                    result = INSTANCE;
    086                    if (result == null) {
    087                        INSTANCE = result = new PluginRegistry();
    088                    }
    089                }
    090            }
    091            return result;
    092        }
    093
    094        /**
    095         * Resets the registry to an empty state.
    096         */
    097        public void clear() {
    098            pluginsByCategoryRef.set(null);
    099            pluginsByCategoryByPackage.clear();
    100            pluginsByCategoryByBundleId.clear();
    101        }
    102
    103        /**
    104         * @since 2.1
    105         */
    106        public Map<Long, Map<String, List<PluginType<?>>>> getPluginsByCategoryByBundleId() {
    107            return pluginsByCategoryByBundleId;
    108        }
    109
    110        /**
    111         * @since 2.1
    112         */
    113        public Map<String, List<PluginType<?>>> loadFromMainClassLoader() {
    114            final Map<String, List<PluginType<?>>> existing = pluginsByCategoryRef.get();
    115            if (existing != null) {
    116                // already loaded
    117                return existing;
    118            }
    119            final Map<String, List<PluginType<?>>> newPluginsByCategory = decodeCacheFiles(Loader.getClassLoader());
    120
    121            // Note multiple threads could be calling this method concurrently. Both will do the work,
    122            // but only one will be allowed to store the result in the AtomicReference.
    123            // Return the map produced by whichever thread won the race, so all callers will get the same result.
    124            if (pluginsByCategoryRef.compareAndSet(null, newPluginsByCategory)) {
    125                return newPluginsByCategory;
    126            }
    127            return pluginsByCategoryRef.get();
    128        }
    129
    130        /**
    131         * @since 2.1
    132         */
    133        public void clearBundlePlugins(final long bundleId) {
    134            pluginsByCategoryByBundleId.remove(bundleId);
    135        }
    136
    137        /**
    138         * @since 2.1
    139         */
    140        public Map<String, List<PluginType<?>>> loadFromBundle(final long bundleId, final ClassLoader loader) {
    141            Map<String, List<PluginType<?>>> existing = pluginsByCategoryByBundleId.get(bundleId);
    142            if (existing != null) {
    143                // already loaded from this classloader
    144                return existing;
    145            }
    146            final Map<String, List<PluginType<?>>> newPluginsByCategory = decodeCacheFiles(loader);
    147
    148            // Note multiple threads could be calling this method concurrently. Both will do the work,
    149            // but only one will be allowed to store the result in the outer map.
    150            // Return the inner map produced by whichever thread won the race, so all callers will get the same result.
    151            existing = pluginsByCategoryByBundleId.putIfAbsent(bundleId, newPluginsByCategory);
    152            if (existing != null) {
    153                return existing;
    154            }
    155            return newPluginsByCategory;
    156        }
    157
    158        private Map<String, List<PluginType<?>>> decodeCacheFiles(final ClassLoader loader) {
    159            final long startTime = System.nanoTime();
    160            final PluginCache cache = new PluginCache();
    161            try {
    162                final Enumeration<URL> resources = loader.getResources(PluginProcessor.PLUGIN_CACHE_FILE);
    163                if (resources == null) {
    164                    LOGGER.info("Plugin preloads not available from class loader {}", loader);
    165                } else {
    166                    cache.loadCacheFiles(resources);
    167                }
    168            } catch (final IOException ioe) {
    169                LOGGER.warn("Unable to preload plugins", ioe);
    170            }
    171            final Map<String, List<PluginType<?>>> newPluginsByCategory = new HashMap<String, List<PluginType<?>>>();
    172            int pluginCount = 0;
    173            for (final Map.Entry<String, Map<String, PluginEntry>> outer : cache.getAllCategories().entrySet()) {
    174                final String categoryLowerCase = outer.getKey();
    175                final List<PluginType<?>> types = new ArrayList<PluginType<?>>(outer.getValue().size());
    176                newPluginsByCategory.put(categoryLowerCase, types);
    177                for (final Map.Entry<String, PluginEntry> inner : outer.getValue().entrySet()) {
    178                    final PluginEntry entry = inner.getValue();
    179                    final String className = entry.getClassName();
    180                    try {
    181                        final Class<?> clazz = loader.loadClass(className);
    182                        @SuppressWarnings({"unchecked","rawtypes"})
    183                        final PluginType<?> type = new PluginType(entry, clazz, entry.getName());
    184                        types.add(type);
    185                        ++pluginCount;
    186                    } catch (final ClassNotFoundException e) {
    187                        LOGGER.info("Plugin [{}] could not be loaded due to missing classes.", className, e);
    188                    } catch (final VerifyError e) {
    189                        LOGGER.info("Plugin [{}] could not be loaded due to verification error.", className, e);
    190                    }
    191                }
    192            }
    193
    194            final long endTime = System.nanoTime();
    195            final DecimalFormat numFormat = new DecimalFormat("#0.000000");
    196            final double seconds = (endTime - startTime) * 1e-9;
    197            LOGGER.debug("Took {} seconds to load {} plugins from {}",
    198                numFormat.format(seconds), pluginCount, loader);
    199            return newPluginsByCategory;
    200        }
    201
    202        /**
    203         * @since 2.1
    204         */
    205        public Map<String, List<PluginType<?>>> loadFromPackage(final String pkg) {
    206            if (Strings.isBlank(pkg)) {
    207                // happens when splitting an empty string
    208                return Collections.emptyMap();
    209            }
    210            Map<String, List<PluginType<?>>> existing = pluginsByCategoryByPackage.get(pkg);
    211            if (existing != null) {
    212                // already loaded this package
    213                return existing;
    214            }
    215
    216            final long startTime = System.nanoTime();
    217            final ResolverUtil resolver = new ResolverUtil();
    218            final ClassLoader classLoader = Loader.getClassLoader();
    219            if (classLoader != null) {
    220                resolver.setClassLoader(classLoader);
    221            }
    222            resolver.findInPackage(new PluginTest(), pkg);
    223
    224            final Map<String, List<PluginType<?>>> newPluginsByCategory = new HashMap<String, List<PluginType<?>>>();
    225            for (final Class<?> clazz : resolver.getClasses()) {
    226                final Plugin plugin = clazz.getAnnotation(Plugin.class);
    227                final String categoryLowerCase = plugin.category().toLowerCase();
    228                List<PluginType<?>> list = newPluginsByCategory.get(categoryLowerCase);
    229                if (list == null) {
    230                    newPluginsByCategory.put(categoryLowerCase, list = new ArrayList<PluginType<?>>());
    231                }
    232                final PluginEntry mainEntry = new PluginEntry();
    233                final String mainElementName = plugin.elementType().equals(
    234                    Plugin.EMPTY) ? plugin.name() : plugin.elementType();
    235                mainEntry.setKey(plugin.name().toLowerCase());
    236                mainEntry.setName(plugin.name());
    237                mainEntry.setCategory(plugin.category());
    238                mainEntry.setClassName(clazz.getName());
    239                mainEntry.setPrintable(plugin.printObject());
    240                mainEntry.setDefer(plugin.deferChildren());
    241                @SuppressWarnings({"unchecked","rawtypes"})
    242                final PluginType<?> mainType = new PluginType(mainEntry, clazz, mainElementName);
    243                list.add(mainType);
    244                final PluginAliases pluginAliases = clazz.getAnnotation(PluginAliases.class);
    245                if (pluginAliases != null) {
    246                    for (final String alias : pluginAliases.value()) {
    247                        final PluginEntry aliasEntry = new PluginEntry();
    248                        final String aliasElementName = plugin.elementType().equals(
    249                            Plugin.EMPTY) ? alias.trim() : plugin.elementType();
    250                        aliasEntry.setKey(alias.trim().toLowerCase());
    251                        aliasEntry.setName(plugin.name());
    252                        aliasEntry.setCategory(plugin.category());
    253                        aliasEntry.setClassName(clazz.getName());
    254                        aliasEntry.setPrintable(plugin.printObject());
    255                        aliasEntry.setDefer(plugin.deferChildren());
    256                        @SuppressWarnings({"unchecked","rawtypes"})
    257                        final PluginType<?> aliasType = new PluginType(aliasEntry, clazz, aliasElementName);
    258                        list.add(aliasType);
    259                    }
    260                }
    261            }
    262
    263            final long endTime = System.nanoTime();
    264            final DecimalFormat numFormat = new DecimalFormat("#0.000000");
    265            final double seconds = (endTime - startTime) * 1e-9;
    266            LOGGER.debug("Took {} seconds to load {} plugins from package {}",
    267                numFormat.format(seconds), resolver.getClasses().size(), pkg);
    268
    269            // Note multiple threads could be calling this method concurrently. Both will do the work,
    270            // but only one will be allowed to store the result in the outer map.
    271            // Return the inner map produced by whichever thread won the race, so all callers will get the same result.
    272            existing = pluginsByCategoryByPackage.putIfAbsent(pkg, newPluginsByCategory);
    273            if (existing != null) {
    274                return existing;
    275            }
    276            return newPluginsByCategory;
    277        }
    278
    279        /**
    280         * A Test that checks to see if each class is annotated with the 'Plugin' annotation. If it
    281         * is, then the test returns true, otherwise false.
    282         *
    283         * @since 2.1
    284         */
    285        public static class PluginTest implements ResolverUtil.Test {
    286            @Override
    287            public boolean matches(final Class<?> type) {
    288                return type != null && type.isAnnotationPresent(Plugin.class);
    289            }
    290
    291            @Override
    292            public String toString() {
    293                return "annotated with @" + Plugin.class.getSimpleName();
    294            }
    295
    296            @Override
    297            public boolean matches(final URI resource) {
    298                throw new UnsupportedOperationException();
    299            }
    300
    301            @Override
    302            public boolean doesMatchClass() {
    303                return true;
    304            }
    305
    306            @Override
    307            public boolean doesMatchResource() {
    308                return false;
    309            }
    310        }
    311    }