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
018package org.apache.logging.log4j.core.config.plugins.util;
019
020import java.io.IOException;
021import java.net.URI;
022import java.net.URL;
023import java.text.DecimalFormat;
024import java.util.ArrayList;
025import java.util.Collections;
026import java.util.Enumeration;
027import java.util.HashMap;
028import java.util.List;
029import java.util.Map;
030import java.util.concurrent.ConcurrentHashMap;
031import java.util.concurrent.ConcurrentMap;
032import java.util.concurrent.atomic.AtomicReference;
033
034import org.apache.logging.log4j.Logger;
035import org.apache.logging.log4j.core.config.plugins.Plugin;
036import org.apache.logging.log4j.core.config.plugins.PluginAliases;
037import org.apache.logging.log4j.core.config.plugins.processor.PluginCache;
038import org.apache.logging.log4j.core.config.plugins.processor.PluginEntry;
039import org.apache.logging.log4j.core.config.plugins.processor.PluginProcessor;
040import org.apache.logging.log4j.core.util.Loader;
041import org.apache.logging.log4j.status.StatusLogger;
042import org.apache.logging.log4j.util.Strings;
043
044/**
045 * Registry singleton for PluginType maps partitioned by source type and then by category names.
046 */
047public 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}