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  package org.apache.logging.log4j.core.config.plugins.util;
18  
19  import java.io.BufferedInputStream;
20  import java.io.DataInputStream;
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.net.URI;
24  import java.net.URL;
25  import java.text.DecimalFormat;
26  import java.util.Collection;
27  import java.util.Enumeration;
28  import java.util.HashMap;
29  import java.util.Map;
30  import java.util.concurrent.ConcurrentMap;
31  import java.util.concurrent.CopyOnWriteArrayList;
32  
33  import org.apache.logging.log4j.Logger;
34  import org.apache.logging.log4j.core.config.plugins.Plugin;
35  import org.apache.logging.log4j.core.config.plugins.PluginAliases;
36  import org.apache.logging.log4j.core.config.plugins.processor.PluginProcessor;
37  import org.apache.logging.log4j.core.util.ClassLoaderResourceLoader;
38  import org.apache.logging.log4j.core.util.Closer;
39  import org.apache.logging.log4j.core.util.Loader;
40  import org.apache.logging.log4j.core.util.ResourceLoader;
41  import org.apache.logging.log4j.status.StatusLogger;
42  import org.apache.logging.log4j.util.Strings;
43  
44  /**
45   * Loads and manages all the plugins.
46   */
47  public class PluginManager {
48  
49      // TODO: re-use PluginCache code from plugin processor
50      private static final PluginRegistry<PluginType<?>> REGISTRY = new PluginRegistry<PluginType<?>>();
51      private static final CopyOnWriteArrayList<String> PACKAGES = new CopyOnWriteArrayList<String>();
52      private static final String LOG4J_PACKAGES = "org.apache.logging.log4j.core";
53  
54      private static final Logger LOGGER = StatusLogger.getLogger();
55  
56      private Map<String, PluginType<?>> plugins = new HashMap<String, PluginType<?>>();
57      private final String category;
58  
59      /**
60       * Constructs a PluginManager for the plugin category name given.
61       * 
62       * @param category The plugin category name.
63       */
64      public PluginManager(final String category) {
65          this.category = category;
66      }
67  
68      /**
69       * Process annotated plugins.
70       * 
71       * @deprecated Use {@link org.apache.logging.log4j.core.config.plugins.processor.PluginProcessor} instead. To do so,
72       *             simply include {@code log4j-core} in your dependencies and make sure annotation processing is not
73       *             disabled. By default, supported Java compilers will automatically use that plugin processor provided
74       *             {@code log4j-core} is on the classpath.
75       */
76      @Deprecated
77      // use PluginProcessor instead
78      public static void main(final String[] args) {
79          System.err.println("ERROR: this tool is superseded by the annotation processor included in log4j-core.");
80          System.err.println("If the annotation processor does not work for you, please see the manual page:");
81          System.err.println("http://logging.apache.org/log4j/2.x/manual/configuration.html#ConfigurationSyntax");
82          System.exit(-1);
83      }
84  
85      /**
86       * Adds a package name to be scanned for plugins. Must be invoked prior to plugins being collected.
87       * 
88       * @param p The package name. Ignored if {@code null} or empty.
89       */
90      public static void addPackage(final String p) {
91          if (Strings.isBlank(p)) {
92              return;
93          }
94          if (PACKAGES.addIfAbsent(p)) {
95              // set of available plugins could have changed, reset plugin cache for newly-retrieved managers
96              REGISTRY.clear(); // TODO confirm if this is correct
97          }
98      }
99  
100     /**
101      * Adds a list of package names to be scanned for plugins. Convenience method for {@link #addPackage(String)}.
102      *
103      * @param packages collection of package names to add. Empty and null package names are ignored.
104      */
105     public static void addPackages(final Collection<String> packages) {
106         for (String pkg : packages) {
107             if (Strings.isNotBlank(pkg)) {
108                 PACKAGES.addIfAbsent(pkg);
109             }
110         }
111     }
112 
113     /**
114      * Returns the type of a specified plugin.
115      * 
116      * @param name The name of the plugin.
117      * @return The plugin's type.
118      */
119     public PluginType<?> getPluginType(final String name) {
120         return plugins.get(name.toLowerCase());
121     }
122 
123     /**
124      * Returns all the matching plugins.
125      * 
126      * @return A Map containing the name of the plugin and its type.
127      */
128     public Map<String, PluginType<?>> getPlugins() {
129         return plugins;
130     }
131 
132     /**
133      * Locates all the plugins.
134      */
135     public void collectPlugins() {
136         collectPlugins(true);
137     }
138 
139     /**
140      * Collects plugins, optionally obtaining them from a preload map.
141      * 
142      * @param preLoad if true, plugins will be obtained from the preload map.
143      *
144      */
145     public void collectPlugins(boolean preLoad) {
146         if (REGISTRY.hasCategory(category)) {
147             plugins = REGISTRY.getCategory(category);
148             preLoad = false;
149         }
150         long start = System.nanoTime();
151         if (preLoad) {
152             final ResourceLoader loader = new ClassLoaderResourceLoader(Loader.getClassLoader());
153             loadPlugins(loader);
154         }
155         plugins = REGISTRY.getCategory(category);
156         loadFromPackages(start, preLoad);
157 
158         long elapsed = System.nanoTime() - start;
159         reportPluginLoadDuration(preLoad, elapsed);
160     }
161     
162     @SuppressWarnings({ "unchecked", "rawtypes" })
163     private void loadFromPackages(final long start, final boolean preLoad) {
164         if (plugins == null || plugins.size() == 0) {
165             if (!PACKAGES.contains(LOG4J_PACKAGES)) {
166                 PACKAGES.add(LOG4J_PACKAGES);
167             }
168         }
169         final ResolverUtil resolver = new ResolverUtil();
170         final ClassLoader classLoader = Loader.getClassLoader();
171         if (classLoader != null) {
172             resolver.setClassLoader(classLoader);
173         }
174         final Class<?> cls = null;
175         final ResolverUtil.Test test = new PluginTest(cls);
176         for (final String pkg : PACKAGES) {
177             resolver.findInPackage(test, pkg);
178         }
179         for (final Class<?> clazz : resolver.getClasses()) {
180             final Plugin plugin = clazz.getAnnotation(Plugin.class);
181             final String pluginCategory = plugin.category();
182             final Map<String, PluginType<?>> map = REGISTRY.getCategory(pluginCategory);
183             String type = plugin.elementType().equals(Plugin.EMPTY) ? plugin.name() : plugin.elementType();
184             PluginType<?> pluginType = new PluginType(clazz, type, plugin.printObject(), plugin.deferChildren());
185             map.put(plugin.name().toLowerCase(), pluginType);
186             final PluginAliases pluginAliases = clazz.getAnnotation(PluginAliases.class);
187             if (pluginAliases != null) {
188                 for (String alias : pluginAliases.value()) {
189                     type =  plugin.elementType().equals(Plugin.EMPTY) ? alias : plugin.elementType();
190                     pluginType = new PluginType(clazz, type, plugin.printObject(), plugin.deferChildren());
191                     map.put(alias.trim().toLowerCase(), pluginType);
192                 }
193             }
194         }
195         plugins = REGISTRY.getCategory(category);
196     }
197 
198     private void reportPluginLoadDuration(final boolean preLoad, long elapsed) {
199         final StringBuilder sb = new StringBuilder("Generated plugins in ");
200         DecimalFormat numFormat = new DecimalFormat("#0.000000");
201         final double seconds = elapsed / (1000.0 * 1000.0 * 1000.0);
202         sb.append(numFormat.format(seconds)).append(" seconds, packages: ");
203         sb.append(PACKAGES);
204         sb.append(", preload: ");
205         sb.append(preLoad);
206         sb.append(".");
207         LOGGER.debug(sb.toString());
208     }
209 
210     public static void loadPlugins(final ResourceLoader loader) {
211         final PluginRegistry<PluginType<?>> registry = decode(loader);
212         if (registry != null) {
213             for (final Map.Entry<String, ConcurrentMap<String, PluginType<?>>> entry : registry.getCategories()) {
214                 REGISTRY.getCategory(entry.getKey()).putAll(entry.getValue());
215             }
216         } else {
217             LOGGER.info("Plugin preloads not available from class loader {}", loader);
218         }
219     }
220 
221     private static PluginRegistry<PluginType<?>> decode(final ResourceLoader loader) {
222         final Enumeration<URL> resources;
223         try {
224             resources = loader.getResources(PluginProcessor.PLUGIN_CACHE_FILE);
225             if (resources == null) {
226                 return null;
227             }
228         } catch (final IOException ioe) {
229             LOGGER.warn("Unable to preload plugins", ioe);
230             return null;
231         }
232         final PluginRegistry<PluginType<?>> map = new PluginRegistry<PluginType<?>>();
233         while (resources.hasMoreElements()) {
234             final URL url = resources.nextElement();
235             LOGGER.debug("Found Plugin Map at {}", url.toExternalForm());
236             final InputStream is;
237             try {
238                 is = url.openStream();
239             } catch (final IOException e) {
240                 LOGGER.warn("Unable to open {}", url.toExternalForm(), e);
241                 continue;
242             }
243             final DataInputStream dis = new DataInputStream(new BufferedInputStream(is));
244             try {
245                 final int count = dis.readInt();
246                 for (int j = 0; j < count; ++j) {
247                     final String category = dis.readUTF();
248                     final int entries = dis.readInt();
249                     final Map<String, PluginType<?>> types = map.getCategory(category);
250                     for (int i = 0; i < entries; ++i) {
251                         final String key = dis.readUTF();
252                         final String className = dis.readUTF();
253                         final String name = dis.readUTF();
254                         final boolean printable = dis.readBoolean();
255                         final boolean defer = dis.readBoolean();
256                         try {
257                             final Class<?> clazz = loader.loadClass(className);
258                             @SuppressWarnings({ "unchecked", "rawtypes" })
259                             final PluginType<?> pluginType = new PluginType(clazz, name, printable, defer);
260                             types.put(key, pluginType);
261                         } catch (final ClassNotFoundException e) {
262                             LOGGER.info("Plugin [{}] could not be loaded due to missing classes.", className, e);
263                         } catch (final VerifyError e) {
264                             LOGGER.info("Plugin [{}] could not be loaded due to verification error.", className, e);
265                         }
266                     }
267                 }
268             } catch (final IOException ex) {
269                 LOGGER.warn("Unable to preload plugins", ex);
270             } finally {
271                 Closer.closeSilently(dis);
272             }
273         }
274         return map.isEmpty() ? null : map;
275     }
276 
277     /**
278      * A Test that checks to see if each class is annotated with a specific annotation. If it
279      * is, then the test returns true, otherwise false.
280      */
281     public static class PluginTest implements ResolverUtil.Test {
282         private final Class<?> isA;
283 
284         /**
285          * Constructs an AnnotatedWith test for the specified annotation type.
286          * @param isA The class to compare against.
287          */
288         public PluginTest(final Class<?> isA) {
289             this.isA = isA;
290         }
291 
292         /**
293          * Returns true if the type is annotated with the class provided to the constructor.
294          * @param type The type to check for.
295          * @return true if the Class is of the specified type.
296          */
297         @Override
298         public boolean matches(final Class<?> type) {
299             return type != null && type.isAnnotationPresent(Plugin.class) &&
300                 (isA == null || isA.isAssignableFrom(type));
301         }
302 
303         @Override
304         public String toString() {
305             final StringBuilder msg = new StringBuilder("annotated with @" + Plugin.class.getSimpleName());
306             if (isA != null) {
307                 msg.append(" is assignable to " + isA.getSimpleName());
308             }
309             return msg.toString();
310         }
311 
312         @Override
313         public boolean matches(final URI resource) {
314             throw new UnsupportedOperationException();
315         }
316 
317         @Override
318         public boolean doesMatchClass() {
319             return true;
320         }
321 
322         @Override
323         public boolean doesMatchResource() {
324             return false;
325         }
326     }
327 
328 }