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 }