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;
018    
019    import org.apache.logging.log4j.Logger;
020    import org.apache.logging.log4j.core.helpers.Loader;
021    import org.apache.logging.log4j.status.StatusLogger;
022    
023    import java.io.BufferedInputStream;
024    import java.io.BufferedOutputStream;
025    import java.io.DataInputStream;
026    import java.io.DataOutputStream;
027    import java.io.File;
028    import java.io.FileOutputStream;
029    import java.io.IOException;
030    import java.io.InputStream;
031    import java.net.URL;
032    import java.text.DecimalFormat;
033    import java.util.Enumeration;
034    import java.util.HashMap;
035    import java.util.Map;
036    import java.util.concurrent.ConcurrentHashMap;
037    import java.util.concurrent.ConcurrentMap;
038    import java.util.concurrent.CopyOnWriteArrayList;
039    
040    /**
041     * Loads and manages all the plugins.
042     */
043    public class PluginManager {
044    
045        private static final long NANOS_PER_SECOND = 1000000000L;
046    
047        private static ConcurrentMap<String, ConcurrentMap<String, PluginType>> pluginTypeMap =
048            new ConcurrentHashMap<String, ConcurrentMap<String, PluginType>>();
049    
050        private static final CopyOnWriteArrayList<String> PACKAGES = new CopyOnWriteArrayList<String>();
051        private static final String PATH = "org/apache/logging/log4j/core/config/plugins/";
052        private static final String FILENAME = "Log4j2Plugins.dat";
053        private static final String LOG4J_PACKAGES = "org.apache.logging.log4j.core";
054    
055        private static final Logger LOGGER = StatusLogger.getLogger();
056    
057        private static String rootDir;
058    
059        private Map<String, PluginType> plugins = new HashMap<String, PluginType>();
060        private final String type;
061        private final Class<?> clazz;
062    
063        /**
064         * Constructor that takes only a type name.
065         * @param type The type name.
066         */
067        public PluginManager(final String type) {
068            this.type = type;
069            this.clazz = null;
070        }
071    
072        /**
073         * Constructor that takes a type name and a Class.
074         * @param type The type that must be matched.
075         * @param clazz The Class each match must be an instance of.
076         */
077        public PluginManager(final String type, final Class<?> clazz) {
078            this.type = type;
079            this.clazz = clazz;
080        }
081    
082        public static void main(final String[] args) throws Exception {
083            if (args == null || args.length < 1) {
084                System.err.println("A target directory must be specified");
085                System.exit(-1);
086            }
087            rootDir = args[0].endsWith("/") || args[0].endsWith("\\") ? args[0] : args[0] + "/";
088    
089            final PluginManager manager = new PluginManager("Core");
090            final String packages = args.length == 2 ? args[1] : null;
091    
092            manager.collectPlugins(false, packages);
093            encode(pluginTypeMap);
094        }
095    
096        /**
097         * Adds a package name to be scanned for plugins. Must be invoked prior to plugins being collected.
098         * @param p The package name.
099         */
100        public static void addPackage(final String p) {
101            if (PACKAGES.addIfAbsent(p))
102            {
103                //set of available plugins could have changed, reset plugin cache for newly-retrieved managers
104                pluginTypeMap.clear();
105            }
106        }
107    
108        /**
109         * Returns the type of a specified plugin.
110         * @param name The name of the plugin.
111         * @return The plugin's type.
112         */
113        public PluginType getPluginType(final String name) {
114            return plugins.get(name.toLowerCase());
115        }
116    
117        /**
118         * Returns all the matching plugins.
119         * @return A Map containing the name of the plugin and its type.
120         */
121        public Map<String, PluginType> getPlugins() {
122            return plugins;
123        }
124    
125        /**
126         * Locates all the plugins.
127         */
128        public void collectPlugins() {
129            collectPlugins(true, null);
130        }
131    
132        /**
133         * Collects plugins, optionally obtaining them from a preload map.
134         * @param preLoad if true, plugins will be obtained from the preload map.
135         * @param pkgs A comma separated list of package names to scan for plugins. If
136         * null the default Log4j package name will be used.
137         */
138        @SuppressWarnings("unchecked")
139        public void collectPlugins(boolean preLoad, final String pkgs) {
140            if (pluginTypeMap.containsKey(type)) {
141                plugins = pluginTypeMap.get(type);
142                preLoad = false;
143            }
144            final long start = System.nanoTime();
145            final ResolverUtil resolver = new ResolverUtil();
146            final ClassLoader loader = Loader.getClassLoader();
147            if (loader != null) {
148                resolver.setClassLoader(loader);
149            }
150            if (preLoad) {
151                final ConcurrentMap<String, ConcurrentMap<String, PluginType>> map = decode(loader);
152                if (map != null) {
153                    pluginTypeMap = map;
154                    plugins = map.get(type);
155                } else {
156                    LOGGER.warn("Plugin preloads not available");
157                }
158            }
159            if (plugins == null || plugins.size() == 0) {
160                if (pkgs == null) {
161                    if (!PACKAGES.contains(LOG4J_PACKAGES)) {
162                        PACKAGES.add(LOG4J_PACKAGES);
163                    }
164                } else {
165                    final String[] names = pkgs.split(",");
166                    for (final String name : names) {
167                        PACKAGES.add(name);
168                    }
169                }
170            }
171            final ResolverUtil.Test test = new PluginTest(clazz);
172            for (final String pkg : PACKAGES) {
173                resolver.findInPackage(test, pkg);
174            }
175            for (final Class<?> clazz : resolver.getClasses()) {
176                final Plugin plugin = clazz.getAnnotation(Plugin.class);
177                final String pluginType = plugin.category();
178                if (!pluginTypeMap.containsKey(pluginType)) {
179                    pluginTypeMap.putIfAbsent(pluginType, new ConcurrentHashMap<String, PluginType>());
180                }
181                final Map<String, PluginType> map = pluginTypeMap.get(pluginType);
182                final String type = plugin.elementType().equals(Plugin.EMPTY) ? plugin.name() : plugin.elementType();
183                map.put(plugin.name().toLowerCase(), new PluginType(clazz, type, plugin.printObject(),
184                    plugin.deferChildren()));
185            }
186            long elapsed = System.nanoTime() - start;
187            plugins = pluginTypeMap.get(type);
188            final StringBuilder sb = new StringBuilder("Generated plugins");
189            sb.append(" in ");
190            DecimalFormat numFormat = new DecimalFormat("#0");
191            final long seconds = elapsed / NANOS_PER_SECOND;
192            elapsed %= NANOS_PER_SECOND;
193            sb.append(numFormat.format(seconds)).append('.');
194            numFormat = new DecimalFormat("000000000");
195            sb.append(numFormat.format(elapsed)).append(" seconds");
196            LOGGER.debug(sb.toString());
197        }
198    
199        @SuppressWarnings("unchecked")
200        private static ConcurrentMap<String, ConcurrentMap<String, PluginType>> decode(final ClassLoader loader) {
201            Enumeration<URL> resources;
202            try {
203                resources = loader.getResources(PATH + FILENAME);
204            } catch (final IOException ioe) {
205                LOGGER.warn("Unable to preload plugins", ioe);
206                return null;
207            }
208            final ConcurrentMap<String, ConcurrentMap<String, PluginType>> map =
209                new ConcurrentHashMap<String, ConcurrentMap<String, PluginType>>();
210            while (resources.hasMoreElements()) {
211                DataInputStream dis = null;
212                try {
213                    final URL url = resources.nextElement();
214                    LOGGER.debug("Found Plugin Map at {}", url.toExternalForm());
215                    final InputStream is = url.openStream();
216                    final BufferedInputStream bis = new BufferedInputStream(is);
217                    dis = new DataInputStream(bis);
218                    final int count = dis.readInt();
219                    for (int j = 0; j < count; ++j) {
220                        final String type = dis.readUTF();
221                        final int entries = dis.readInt();
222                        ConcurrentMap<String, PluginType> types = map.get(type);
223                        if (types == null) {
224                            types = new ConcurrentHashMap<String, PluginType>(count);
225                        }
226                        for (int i = 0; i < entries; ++i) {
227                            final String key = dis.readUTF();
228                            final String className = dis.readUTF();
229                            final String name = dis.readUTF();
230                            final boolean printable = dis.readBoolean();
231                            final boolean defer = dis.readBoolean();
232                            final Class<?> clazz = Class.forName(className);
233                            types.put(key, new PluginType(clazz, name, printable, defer));
234                        }
235                        map.putIfAbsent(type, types);
236                    }
237                } catch (final Exception ex) {
238                    LOGGER.warn("Unable to preload plugins", ex);
239                    return null;
240                } finally {
241                    try {
242                        dis.close();
243                    } catch (Exception ignored) {
244                        // nothing we can do here...
245                    }
246                }
247            }
248            return map.size() == 0 ? null : map;
249        }
250    
251        private static void encode(final ConcurrentMap<String, ConcurrentMap<String, PluginType>> map) {
252            final String fileName = rootDir + PATH + FILENAME;
253            DataOutputStream dos = null;
254            try {
255                final File file = new File(rootDir + PATH);
256                file.mkdirs();
257                final FileOutputStream fos = new FileOutputStream(fileName);
258                final BufferedOutputStream bos = new BufferedOutputStream(fos);
259                dos = new DataOutputStream(bos);
260                dos.writeInt(map.size());
261                for (final Map.Entry<String, ConcurrentMap<String, PluginType>> outer : map.entrySet()) {
262                    dos.writeUTF(outer.getKey());
263                    dos.writeInt(outer.getValue().size());
264                    for (final Map.Entry<String, PluginType> entry : outer.getValue().entrySet()) {
265                        dos.writeUTF(entry.getKey());
266                        final PluginType pt = entry.getValue();
267                        dos.writeUTF(pt.getPluginClass().getName());
268                        dos.writeUTF(pt.getElementName());
269                        dos.writeBoolean(pt.isObjectPrintable());
270                        dos.writeBoolean(pt.isDeferChildren());
271                    }
272                }
273            } catch (final Exception ex) {
274                ex.printStackTrace();
275            } finally {
276                try {
277                    dos.close();
278                } catch (Exception ignored) {
279                    // nothing we can do here...
280                }
281            }
282        }
283    
284        /**
285         * A Test that checks to see if each class is annotated with a specific annotation. If it
286         * is, then the test returns true, otherwise false.
287         */
288        public static class PluginTest extends ResolverUtil.ClassTest {
289            private final Class<?> isA;
290    
291            /**
292             * Constructs an AnnotatedWith test for the specified annotation type.
293             * @param isA The class to compare against.
294             */
295            public PluginTest(final Class<?> isA) {
296                this.isA = isA;
297            }
298    
299            /**
300             * Returns true if the type is annotated with the class provided to the constructor.
301             * @param type The type to check for.
302             * @return true if the Class is of the specified type.
303             */
304            @Override
305            public boolean matches(final Class<?> type) {
306                return type != null && type.isAnnotationPresent(Plugin.class) &&
307                    (isA == null || isA.isAssignableFrom(type));
308            }
309    
310            @Override
311            public String toString() {
312                final StringBuilder msg = new StringBuilder("annotated with @" + Plugin.class.getSimpleName());
313                if (isA != null) {
314                    msg.append(" is assignable to " + isA.getSimpleName());
315                }
316                return msg.toString();
317            }
318        }
319    
320    }