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}