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    }