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    package org.apache.logging.log4j.core.config.plugins.util;
018    
019    import java.io.BufferedInputStream;
020    import java.io.DataInputStream;
021    import java.io.IOException;
022    import java.io.InputStream;
023    import java.net.URI;
024    import java.net.URL;
025    import java.text.DecimalFormat;
026    import java.util.Collection;
027    import java.util.Enumeration;
028    import java.util.HashMap;
029    import java.util.Map;
030    import java.util.concurrent.ConcurrentMap;
031    import java.util.concurrent.CopyOnWriteArrayList;
032    
033    import org.apache.logging.log4j.Logger;
034    import org.apache.logging.log4j.core.config.plugins.Plugin;
035    import org.apache.logging.log4j.core.config.plugins.PluginAliases;
036    import org.apache.logging.log4j.core.config.plugins.processor.PluginProcessor;
037    import org.apache.logging.log4j.core.util.ClassLoaderResourceLoader;
038    import org.apache.logging.log4j.core.util.Closer;
039    import org.apache.logging.log4j.core.util.Loader;
040    import org.apache.logging.log4j.core.util.ResourceLoader;
041    import org.apache.logging.log4j.status.StatusLogger;
042    import org.apache.logging.log4j.util.Strings;
043    
044    /**
045     * Loads and manages all the plugins.
046     */
047    public class PluginManager {
048    
049        // TODO: re-use PluginCache code from plugin processor
050        private static final PluginRegistry<PluginType<?>> REGISTRY = new PluginRegistry<PluginType<?>>();
051        private static final CopyOnWriteArrayList<String> PACKAGES = new CopyOnWriteArrayList<String>();
052        private static final String LOG4J_PACKAGES = "org.apache.logging.log4j.core";
053    
054        private static final Logger LOGGER = StatusLogger.getLogger();
055    
056        private Map<String, PluginType<?>> plugins = new HashMap<String, PluginType<?>>();
057        private final String category;
058    
059        /**
060         * Constructs a PluginManager for the plugin category name given.
061         * 
062         * @param category The plugin category name.
063         */
064        public PluginManager(final String category) {
065            this.category = category;
066        }
067    
068        /**
069         * Process annotated plugins.
070         * 
071         * @deprecated Use {@link org.apache.logging.log4j.core.config.plugins.processor.PluginProcessor} instead. To do so,
072         *             simply include {@code log4j-core} in your dependencies and make sure annotation processing is not
073         *             disabled. By default, supported Java compilers will automatically use that plugin processor provided
074         *             {@code log4j-core} is on the classpath.
075         */
076        @Deprecated
077        // use PluginProcessor instead
078        public static void main(final String[] args) {
079            System.err.println("ERROR: this tool is superseded by the annotation processor included in log4j-core.");
080            System.err.println("If the annotation processor does not work for you, please see the manual page:");
081            System.err.println("http://logging.apache.org/log4j/2.x/manual/configuration.html#ConfigurationSyntax");
082            System.exit(-1);
083        }
084    
085        /**
086         * Adds a package name to be scanned for plugins. Must be invoked prior to plugins being collected.
087         * 
088         * @param p The package name. Ignored if {@code null} or empty.
089         */
090        public static void addPackage(final String p) {
091            if (Strings.isBlank(p)) {
092                return;
093            }
094            if (PACKAGES.addIfAbsent(p)) {
095                // set of available plugins could have changed, reset plugin cache for newly-retrieved managers
096                REGISTRY.clear(); // TODO confirm if this is correct
097            }
098        }
099    
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    }