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<>(); 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<>(); 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<>(); 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<>(); 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<>(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 final PluginType<?> type = new PluginType<>(entry, clazz, entry.getName()); 183 types.add(type); 184 ++pluginCount; 185 } catch (final ClassNotFoundException e) { 186 LOGGER.info("Plugin [{}] could not be loaded due to missing classes.", className, e); 187 } catch (final LinkageError e) { 188 LOGGER.info("Plugin [{}] could not be loaded due to linkage error.", className, e); 189 } 190 } 191 } 192 193 final long endTime = System.nanoTime(); 194 final DecimalFormat numFormat = new DecimalFormat("#0.000000"); 195 final double seconds = (endTime - startTime) * 1e-9; 196 LOGGER.debug("Took {} seconds to load {} plugins from {}", 197 numFormat.format(seconds), pluginCount, loader); 198 return newPluginsByCategory; 199 } 200 201 /** 202 * @since 2.1 203 */ 204 public Map<String, List<PluginType<?>>> loadFromPackage(final String pkg) { 205 if (Strings.isBlank(pkg)) { 206 // happens when splitting an empty string 207 return Collections.emptyMap(); 208 } 209 Map<String, List<PluginType<?>>> existing = pluginsByCategoryByPackage.get(pkg); 210 if (existing != null) { 211 // already loaded this package 212 return existing; 213 } 214 215 final long startTime = System.nanoTime(); 216 final ResolverUtil resolver = new ResolverUtil(); 217 final ClassLoader classLoader = Loader.getClassLoader(); 218 if (classLoader != null) { 219 resolver.setClassLoader(classLoader); 220 } 221 resolver.findInPackage(new PluginTest(), pkg); 222 223 final Map<String, List<PluginType<?>>> newPluginsByCategory = new HashMap<>(); 224 for (final Class<?> clazz : resolver.getClasses()) { 225 final Plugin plugin = clazz.getAnnotation(Plugin.class); 226 final String categoryLowerCase = plugin.category().toLowerCase(); 227 List<PluginType<?>> list = newPluginsByCategory.get(categoryLowerCase); 228 if (list == null) { 229 newPluginsByCategory.put(categoryLowerCase, list = new ArrayList<>()); 230 } 231 final PluginEntry mainEntry = new PluginEntry(); 232 final String mainElementName = plugin.elementType().equals( 233 Plugin.EMPTY) ? plugin.name() : plugin.elementType(); 234 mainEntry.setKey(plugin.name().toLowerCase()); 235 mainEntry.setName(plugin.name()); 236 mainEntry.setCategory(plugin.category()); 237 mainEntry.setClassName(clazz.getName()); 238 mainEntry.setPrintable(plugin.printObject()); 239 mainEntry.setDefer(plugin.deferChildren()); 240 final PluginType<?> mainType = new PluginType<>(mainEntry, clazz, mainElementName); 241 list.add(mainType); 242 final PluginAliases pluginAliases = clazz.getAnnotation(PluginAliases.class); 243 if (pluginAliases != null) { 244 for (final String alias : pluginAliases.value()) { 245 final PluginEntry aliasEntry = new PluginEntry(); 246 final String aliasElementName = plugin.elementType().equals( 247 Plugin.EMPTY) ? alias.trim() : plugin.elementType(); 248 aliasEntry.setKey(alias.trim().toLowerCase()); 249 aliasEntry.setName(plugin.name()); 250 aliasEntry.setCategory(plugin.category()); 251 aliasEntry.setClassName(clazz.getName()); 252 aliasEntry.setPrintable(plugin.printObject()); 253 aliasEntry.setDefer(plugin.deferChildren()); 254 final PluginType<?> aliasType = new PluginType<>(aliasEntry, clazz, aliasElementName); 255 list.add(aliasType); 256 } 257 } 258 } 259 260 final long endTime = System.nanoTime(); 261 final DecimalFormat numFormat = new DecimalFormat("#0.000000"); 262 final double seconds = (endTime - startTime) * 1e-9; 263 LOGGER.debug("Took {} seconds to load {} plugins from package {}", 264 numFormat.format(seconds), resolver.getClasses().size(), pkg); 265 266 // Note multiple threads could be calling this method concurrently. Both will do the work, 267 // but only one will be allowed to store the result in the outer map. 268 // Return the inner map produced by whichever thread won the race, so all callers will get the same result. 269 existing = pluginsByCategoryByPackage.putIfAbsent(pkg, newPluginsByCategory); 270 if (existing != null) { 271 return existing; 272 } 273 return newPluginsByCategory; 274 } 275 276 /** 277 * A Test that checks to see if each class is annotated with the 'Plugin' annotation. If it 278 * is, then the test returns true, otherwise false. 279 * 280 * @since 2.1 281 */ 282 public static class PluginTest implements ResolverUtil.Test { 283 @Override 284 public boolean matches(final Class<?> type) { 285 return type != null && type.isAnnotationPresent(Plugin.class); 286 } 287 288 @Override 289 public String toString() { 290 return "annotated with @" + Plugin.class.getSimpleName(); 291 } 292 293 @Override 294 public boolean matches(final URI resource) { 295 throw new UnsupportedOperationException(); 296 } 297 298 @Override 299 public boolean doesMatchClass() { 300 return true; 301 } 302 303 @Override 304 public boolean doesMatchResource() { 305 return false; 306 } 307 } 308}