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 018package org.apache.logging.log4j.core.config.plugins.util; 019 020import java.io.IOException; 021import java.net.URI; 022import java.net.URL; 023import java.text.DecimalFormat; 024import java.util.ArrayList; 025import java.util.Collections; 026import java.util.Enumeration; 027import java.util.HashMap; 028import java.util.List; 029import java.util.Map; 030import java.util.concurrent.ConcurrentHashMap; 031import java.util.concurrent.ConcurrentMap; 032import java.util.concurrent.atomic.AtomicReference; 033 034import org.apache.logging.log4j.Logger; 035import org.apache.logging.log4j.core.config.plugins.Plugin; 036import org.apache.logging.log4j.core.config.plugins.PluginAliases; 037import org.apache.logging.log4j.core.config.plugins.processor.PluginCache; 038import org.apache.logging.log4j.core.config.plugins.processor.PluginEntry; 039import org.apache.logging.log4j.core.config.plugins.processor.PluginProcessor; 040import org.apache.logging.log4j.core.util.Loader; 041import org.apache.logging.log4j.status.StatusLogger; 042import org.apache.logging.log4j.util.Strings; 043 044/** 045 * Registry singleton for PluginType maps partitioned by source type and then by category names. 046 */ 047public class PluginRegistry { 048 049 private static final Logger LOGGER = StatusLogger.getLogger(); 050 051 private static volatile PluginRegistry INSTANCE; 052 private static final Object INSTANCE_LOCK = new Object(); 053 054 /** 055 * Contains plugins found in Log4j2Plugins.dat cache files in the main CLASSPATH. 056 */ 057 private final AtomicReference<Map<String, List<PluginType<?>>>> pluginsByCategoryRef = 058 new AtomicReference<Map<String, List<PluginType<?>>>>(); 059 060 /** 061 * Contains plugins found in Log4j2Plugins.dat cache files in OSGi Bundles. 062 */ 063 private final ConcurrentMap<Long, Map<String, List<PluginType<?>>>> pluginsByCategoryByBundleId = 064 new ConcurrentHashMap<Long, Map<String, List<PluginType<?>>>>(); 065 066 /** 067 * Contains plugins found by searching for annotated classes at runtime. 068 */ 069 private final ConcurrentMap<String, Map<String, List<PluginType<?>>>> pluginsByCategoryByPackage = 070 new ConcurrentHashMap<String, Map<String, List<PluginType<?>>>>(); 071 072 private PluginRegistry() { 073 } 074 075 /** 076 * Returns the global PluginRegistry instance. 077 * 078 * @return the global PluginRegistry instance. 079 * @since 2.1 080 */ 081 public static PluginRegistry getInstance() { 082 PluginRegistry result = INSTANCE; 083 if (result == null) { 084 synchronized (INSTANCE_LOCK) { 085 result = INSTANCE; 086 if (result == null) { 087 INSTANCE = result = new PluginRegistry(); 088 } 089 } 090 } 091 return result; 092 } 093 094 /** 095 * Resets the registry to an empty state. 096 */ 097 public void clear() { 098 pluginsByCategoryRef.set(null); 099 pluginsByCategoryByPackage.clear(); 100 pluginsByCategoryByBundleId.clear(); 101 } 102 103 /** 104 * @since 2.1 105 */ 106 public Map<Long, Map<String, List<PluginType<?>>>> getPluginsByCategoryByBundleId() { 107 return pluginsByCategoryByBundleId; 108 } 109 110 /** 111 * @since 2.1 112 */ 113 public Map<String, List<PluginType<?>>> loadFromMainClassLoader() { 114 final Map<String, List<PluginType<?>>> existing = pluginsByCategoryRef.get(); 115 if (existing != null) { 116 // already loaded 117 return existing; 118 } 119 final Map<String, List<PluginType<?>>> newPluginsByCategory = decodeCacheFiles(Loader.getClassLoader()); 120 121 // Note multiple threads could be calling this method concurrently. Both will do the work, 122 // but only one will be allowed to store the result in the AtomicReference. 123 // Return the map produced by whichever thread won the race, so all callers will get the same result. 124 if (pluginsByCategoryRef.compareAndSet(null, newPluginsByCategory)) { 125 return newPluginsByCategory; 126 } 127 return pluginsByCategoryRef.get(); 128 } 129 130 /** 131 * @since 2.1 132 */ 133 public void clearBundlePlugins(final long bundleId) { 134 pluginsByCategoryByBundleId.remove(bundleId); 135 } 136 137 /** 138 * @since 2.1 139 */ 140 public Map<String, List<PluginType<?>>> loadFromBundle(final long bundleId, final ClassLoader loader) { 141 Map<String, List<PluginType<?>>> existing = pluginsByCategoryByBundleId.get(bundleId); 142 if (existing != null) { 143 // already loaded from this classloader 144 return existing; 145 } 146 final Map<String, List<PluginType<?>>> newPluginsByCategory = decodeCacheFiles(loader); 147 148 // Note multiple threads could be calling this method concurrently. Both will do the work, 149 // but only one will be allowed to store the result in the outer map. 150 // Return the inner map produced by whichever thread won the race, so all callers will get the same result. 151 existing = pluginsByCategoryByBundleId.putIfAbsent(bundleId, newPluginsByCategory); 152 if (existing != null) { 153 return existing; 154 } 155 return newPluginsByCategory; 156 } 157 158 private Map<String, List<PluginType<?>>> decodeCacheFiles(final ClassLoader loader) { 159 final long startTime = System.nanoTime(); 160 final PluginCache cache = new PluginCache(); 161 try { 162 final Enumeration<URL> resources = loader.getResources(PluginProcessor.PLUGIN_CACHE_FILE); 163 if (resources == null) { 164 LOGGER.info("Plugin preloads not available from class loader {}", loader); 165 } else { 166 cache.loadCacheFiles(resources); 167 } 168 } catch (final IOException ioe) { 169 LOGGER.warn("Unable to preload plugins", ioe); 170 } 171 final Map<String, List<PluginType<?>>> newPluginsByCategory = new HashMap<String, List<PluginType<?>>>(); 172 int pluginCount = 0; 173 for (final Map.Entry<String, Map<String, PluginEntry>> outer : cache.getAllCategories().entrySet()) { 174 final String categoryLowerCase = outer.getKey(); 175 final List<PluginType<?>> types = new ArrayList<PluginType<?>>(outer.getValue().size()); 176 newPluginsByCategory.put(categoryLowerCase, types); 177 for (final Map.Entry<String, PluginEntry> inner : outer.getValue().entrySet()) { 178 final PluginEntry entry = inner.getValue(); 179 final String className = entry.getClassName(); 180 try { 181 final Class<?> clazz = loader.loadClass(className); 182 @SuppressWarnings({"unchecked","rawtypes"}) 183 final PluginType<?> type = new PluginType(entry, clazz, entry.getName()); 184 types.add(type); 185 ++pluginCount; 186 } catch (final ClassNotFoundException e) { 187 LOGGER.info("Plugin [{}] could not be loaded due to missing classes.", className, e); 188 } catch (final VerifyError e) { 189 LOGGER.info("Plugin [{}] could not be loaded due to verification error.", className, e); 190 } 191 } 192 } 193 194 final long endTime = System.nanoTime(); 195 final DecimalFormat numFormat = new DecimalFormat("#0.000000"); 196 final double seconds = (endTime - startTime) * 1e-9; 197 LOGGER.debug("Took {} seconds to load {} plugins from {}", 198 numFormat.format(seconds), pluginCount, loader); 199 return newPluginsByCategory; 200 } 201 202 /** 203 * @since 2.1 204 */ 205 public Map<String, List<PluginType<?>>> loadFromPackage(final String pkg) { 206 if (Strings.isBlank(pkg)) { 207 // happens when splitting an empty string 208 return Collections.emptyMap(); 209 } 210 Map<String, List<PluginType<?>>> existing = pluginsByCategoryByPackage.get(pkg); 211 if (existing != null) { 212 // already loaded this package 213 return existing; 214 } 215 216 final long startTime = System.nanoTime(); 217 final ResolverUtil resolver = new ResolverUtil(); 218 final ClassLoader classLoader = Loader.getClassLoader(); 219 if (classLoader != null) { 220 resolver.setClassLoader(classLoader); 221 } 222 resolver.findInPackage(new PluginTest(), pkg); 223 224 final Map<String, List<PluginType<?>>> newPluginsByCategory = new HashMap<String, List<PluginType<?>>>(); 225 for (final Class<?> clazz : resolver.getClasses()) { 226 final Plugin plugin = clazz.getAnnotation(Plugin.class); 227 final String categoryLowerCase = plugin.category().toLowerCase(); 228 List<PluginType<?>> list = newPluginsByCategory.get(categoryLowerCase); 229 if (list == null) { 230 newPluginsByCategory.put(categoryLowerCase, list = new ArrayList<PluginType<?>>()); 231 } 232 final PluginEntry mainEntry = new PluginEntry(); 233 final String mainElementName = plugin.elementType().equals( 234 Plugin.EMPTY) ? plugin.name() : plugin.elementType(); 235 mainEntry.setKey(plugin.name().toLowerCase()); 236 mainEntry.setName(plugin.name()); 237 mainEntry.setCategory(plugin.category()); 238 mainEntry.setClassName(clazz.getName()); 239 mainEntry.setPrintable(plugin.printObject()); 240 mainEntry.setDefer(plugin.deferChildren()); 241 @SuppressWarnings({"unchecked","rawtypes"}) 242 final PluginType<?> mainType = new PluginType(mainEntry, clazz, mainElementName); 243 list.add(mainType); 244 final PluginAliases pluginAliases = clazz.getAnnotation(PluginAliases.class); 245 if (pluginAliases != null) { 246 for (final String alias : pluginAliases.value()) { 247 final PluginEntry aliasEntry = new PluginEntry(); 248 final String aliasElementName = plugin.elementType().equals( 249 Plugin.EMPTY) ? alias.trim() : plugin.elementType(); 250 aliasEntry.setKey(alias.trim().toLowerCase()); 251 aliasEntry.setName(plugin.name()); 252 aliasEntry.setCategory(plugin.category()); 253 aliasEntry.setClassName(clazz.getName()); 254 aliasEntry.setPrintable(plugin.printObject()); 255 aliasEntry.setDefer(plugin.deferChildren()); 256 @SuppressWarnings({"unchecked","rawtypes"}) 257 final PluginType<?> aliasType = new PluginType(aliasEntry, clazz, aliasElementName); 258 list.add(aliasType); 259 } 260 } 261 } 262 263 final long endTime = System.nanoTime(); 264 final DecimalFormat numFormat = new DecimalFormat("#0.000000"); 265 final double seconds = (endTime - startTime) * 1e-9; 266 LOGGER.debug("Took {} seconds to load {} plugins from package {}", 267 numFormat.format(seconds), resolver.getClasses().size(), pkg); 268 269 // Note multiple threads could be calling this method concurrently. Both will do the work, 270 // but only one will be allowed to store the result in the outer map. 271 // Return the inner map produced by whichever thread won the race, so all callers will get the same result. 272 existing = pluginsByCategoryByPackage.putIfAbsent(pkg, newPluginsByCategory); 273 if (existing != null) { 274 return existing; 275 } 276 return newPluginsByCategory; 277 } 278 279 /** 280 * A Test that checks to see if each class is annotated with the 'Plugin' annotation. If it 281 * is, then the test returns true, otherwise false. 282 * 283 * @since 2.1 284 */ 285 public static class PluginTest implements ResolverUtil.Test { 286 @Override 287 public boolean matches(final Class<?> type) { 288 return type != null && type.isAnnotationPresent(Plugin.class); 289 } 290 291 @Override 292 public String toString() { 293 return "annotated with @" + Plugin.class.getSimpleName(); 294 } 295 296 @Override 297 public boolean matches(final URI resource) { 298 throw new UnsupportedOperationException(); 299 } 300 301 @Override 302 public boolean doesMatchClass() { 303 return true; 304 } 305 306 @Override 307 public boolean doesMatchResource() { 308 return false; 309 } 310 } 311}