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