View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.logging.log4j.core.config.plugins.util;
19  
20  import java.io.IOException;
21  import java.net.URI;
22  import java.net.URL;
23  import java.text.DecimalFormat;
24  import java.util.ArrayList;
25  import java.util.Collections;
26  import java.util.Enumeration;
27  import java.util.HashMap;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.concurrent.ConcurrentHashMap;
31  import java.util.concurrent.ConcurrentMap;
32  import java.util.concurrent.atomic.AtomicReference;
33  
34  import org.apache.logging.log4j.Logger;
35  import org.apache.logging.log4j.core.config.plugins.Plugin;
36  import org.apache.logging.log4j.core.config.plugins.PluginAliases;
37  import org.apache.logging.log4j.core.config.plugins.processor.PluginCache;
38  import org.apache.logging.log4j.core.config.plugins.processor.PluginEntry;
39  import org.apache.logging.log4j.core.config.plugins.processor.PluginProcessor;
40  import org.apache.logging.log4j.core.util.Loader;
41  import org.apache.logging.log4j.status.StatusLogger;
42  import org.apache.logging.log4j.util.Strings;
43  
44  /**
45   * Registry singleton for PluginType maps partitioned by source type and then by category names.
46   */
47  public class PluginRegistry {
48  
49      private static final Logger LOGGER = StatusLogger.getLogger();
50  
51      private static volatile PluginRegistry INSTANCE;
52      private static final Object INSTANCE_LOCK = new Object();
53  
54      /**
55       * Contains plugins found in Log4j2Plugins.dat cache files in the main CLASSPATH.
56       */
57      private final AtomicReference<Map<String, List<PluginType<?>>>> pluginsByCategoryRef =
58          new AtomicReference<Map<String, List<PluginType<?>>>>();
59  
60      /**
61       * Contains plugins found in Log4j2Plugins.dat cache files in OSGi Bundles.
62       */
63      private final ConcurrentMap<Long, Map<String, List<PluginType<?>>>> pluginsByCategoryByBundleId =
64          new ConcurrentHashMap<Long, Map<String, List<PluginType<?>>>>();
65  
66      /**
67       * Contains plugins found by searching for annotated classes at runtime.
68       */
69      private final ConcurrentMap<String, Map<String, List<PluginType<?>>>> pluginsByCategoryByPackage =
70          new ConcurrentHashMap<String, Map<String, List<PluginType<?>>>>();
71  
72      private PluginRegistry() {
73      }
74  
75      /**
76       * Returns the global PluginRegistry instance.
77       *
78       * @return the global PluginRegistry instance.
79       * @since 2.1
80       */
81      public static PluginRegistry getInstance() {
82          PluginRegistry result = INSTANCE;
83          if (result == null) {
84              synchronized (INSTANCE_LOCK) {
85                  result = INSTANCE;
86                  if (result == null) {
87                      INSTANCE = result = new PluginRegistry();
88                  }
89              }
90          }
91          return result;
92      }
93  
94      /**
95       * Resets the registry to an empty state.
96       */
97      public void clear() {
98          pluginsByCategoryRef.set(null);
99          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 }