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