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
018 package org.apache.logging.log4j.core.config.plugins.util;
019
020 import java.io.IOException;
021 import java.net.URI;
022 import java.net.URL;
023 import java.text.DecimalFormat;
024 import java.util.ArrayList;
025 import java.util.Collections;
026 import java.util.Enumeration;
027 import java.util.HashMap;
028 import java.util.List;
029 import java.util.Map;
030 import java.util.concurrent.ConcurrentHashMap;
031 import java.util.concurrent.ConcurrentMap;
032 import java.util.concurrent.atomic.AtomicReference;
033
034 import org.apache.logging.log4j.Logger;
035 import org.apache.logging.log4j.core.config.plugins.Plugin;
036 import org.apache.logging.log4j.core.config.plugins.PluginAliases;
037 import org.apache.logging.log4j.core.config.plugins.processor.PluginCache;
038 import org.apache.logging.log4j.core.config.plugins.processor.PluginEntry;
039 import org.apache.logging.log4j.core.config.plugins.processor.PluginProcessor;
040 import org.apache.logging.log4j.core.util.Loader;
041 import org.apache.logging.log4j.status.StatusLogger;
042 import org.apache.logging.log4j.util.Strings;
043
044 /**
045 * Registry singleton for PluginType maps partitioned by source type and then by category names.
046 */
047 public 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 }