/* * Copyright (C) 2012-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.pf4j; import org.pf4j.asm.ExtensionInfo; import org.pf4j.util.ClassUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; /** * @author Decebal Suiu */ public abstract class AbstractExtensionFinder implements ExtensionFinder, PluginStateListener { private static final Logger log = LoggerFactory.getLogger(AbstractExtensionFinder.class); protected PluginManager pluginManager; protected volatile Map> entries; // cache by pluginId protected volatile Map extensionInfos; // cache extension infos by class name protected Boolean checkForExtensionDependencies = null; protected AbstractExtensionFinder(PluginManager pluginManager) { this.pluginManager = pluginManager; } public abstract Map> readPluginsStorages(); public abstract Map> readClasspathStorages(); @Override public List> find(Class type) { log.debug("Finding extensions of extension point '{}'", type.getName()); Map> entries = getEntries(); List> result = new ArrayList<>(); // add extensions found in classpath and plugins for (String pluginId : entries.keySet()) { // classpath's extensions <=> pluginId = null List> pluginExtensions = find(type, pluginId); result.addAll(pluginExtensions); } if (result.isEmpty()) { log.debug("No extensions found for extension point '{}'", type.getName()); } else { log.debug("Found {} extensions for extension point '{}'", result.size(), type.getName()); } // sort by "ordinal" property Collections.sort(result); return result; } @Override @SuppressWarnings("unchecked") public List> find(Class type, String pluginId) { log.debug("Finding extensions of extension point '{}' for plugin '{}'", type.getName(), pluginId); List> result = new ArrayList<>(); // classpath's extensions <=> pluginId = null Set classNames = findClassNames(pluginId); if (classNames.isEmpty()) { return result; } if (pluginId != null) { PluginWrapper pluginWrapper = pluginManager.getPlugin(pluginId); if (PluginState.STARTED != pluginWrapper.getPluginState()) { return result; } log.trace("Checking extensions from plugin '{}'", pluginId); } else { log.trace("Checking extensions from classpath"); } ClassLoader classLoader = (pluginId != null) ? pluginManager.getPluginClassLoader(pluginId) : getClass().getClassLoader(); for (String className : classNames) { try { if (isCheckForExtensionDependencies()) { // Load extension annotation without initializing the class itself. // // If optional dependencies are used, the class loader might not be able // to load the extension class because of missing optional dependencies. // // Therefore we're extracting the extension annotation via asm, in order // to extract the required plugins for an extension. Only if all required // plugins are currently available and started, the corresponding // extension is loaded through the class loader. ExtensionInfo extensionInfo = getExtensionInfo(className, classLoader); if (extensionInfo == null) { log.error("No extension annotation was found for '{}'", className); continue; } // Make sure, that all plugins required by this extension are available. List missingPluginIds = new ArrayList<>(); for (String requiredPluginId : extensionInfo.getPlugins()) { PluginWrapper requiredPlugin = pluginManager.getPlugin(requiredPluginId); if (requiredPlugin == null || !PluginState.STARTED.equals(requiredPlugin.getPluginState())) { missingPluginIds.add(requiredPluginId); } } if (!missingPluginIds.isEmpty()) { StringBuilder missing = new StringBuilder(); for (String missingPluginId : missingPluginIds) { if (missing.length() > 0) missing.append(", "); missing.append(missingPluginId); } log.trace("Extension '{}' is ignored due to missing plugins: {}", className, missing); continue; } } log.debug("Loading class '{}' using class loader '{}'", className, classLoader); Class extensionClass = classLoader.loadClass(className); log.debug("Checking extension type '{}'", className); if (type.isAssignableFrom(extensionClass)) { ExtensionWrapper extensionWrapper = createExtensionWrapper(extensionClass); result.add(extensionWrapper); log.debug("Added extension '{}' with ordinal {}", className, extensionWrapper.getOrdinal()); } else { log.trace("'{}' is not an extension for extension point '{}'", className, type.getName()); if (RuntimeMode.DEVELOPMENT.equals(pluginManager.getRuntimeMode())) { checkDifferentClassLoaders(type, extensionClass); } } } catch (ClassNotFoundException | NoClassDefFoundError e) { log.error(e.getMessage(), e); } } if (result.isEmpty()) { log.debug("No extensions found for extension point '{}'", type.getName()); } else { log.debug("Found {} extensions for extension point '{}'", result.size(), type.getName()); } // sort by "ordinal" property Collections.sort(result); return result; } @Override public List find(String pluginId) { log.debug("Finding extensions from plugin '{}'", pluginId); List result = new ArrayList<>(); Set classNames = findClassNames(pluginId); if (classNames.isEmpty()) { return result; } if (pluginId != null) { PluginWrapper pluginWrapper = pluginManager.getPlugin(pluginId); if (PluginState.STARTED != pluginWrapper.getPluginState()) { return result; } log.trace("Checking extensions from plugin '{}'", pluginId); } else { log.trace("Checking extensions from classpath"); } ClassLoader classLoader = (pluginId != null) ? pluginManager.getPluginClassLoader(pluginId) : getClass().getClassLoader(); for (String className : classNames) { try { log.debug("Loading class '{}' using class loader '{}'", className, classLoader); Class extensionClass = classLoader.loadClass(className); ExtensionWrapper extensionWrapper = createExtensionWrapper(extensionClass); result.add(extensionWrapper); log.debug("Added extension '{}' with ordinal {}", className, extensionWrapper.getOrdinal()); } catch (ClassNotFoundException | NoClassDefFoundError e) { log.error(e.getMessage(), e); } } if (result.isEmpty()) { log.debug("No extensions found for plugin '{}'", pluginId); } else { log.debug("Found {} extensions for plugin '{}'", result.size(), pluginId); } // sort by "ordinal" property Collections.sort(result); return result; } @Override public Set findClassNames(String pluginId) { Set classNames = getEntries().get(pluginId); if (classNames == null) { return Collections.emptySet(); } return classNames; } @Override public void pluginStateChanged(PluginStateEvent event) { // TODO optimize (do only for some transitions) // clear cache entries = null; // By default we're assuming, that no checks for extension dependencies are necessary. // // A plugin, that has an optional dependency to other plugins, might lead to unloadable // Java classes (NoClassDefFoundError) at application runtime due to possibly missing // dependencies. Therefore we're enabling the check for optional extensions, if the // started plugin contains at least one optional plugin dependency. if (checkForExtensionDependencies == null && PluginState.STARTED.equals(event.getPluginState())) { for (PluginDependency dependency : event.getPlugin().getDescriptor().getDependencies()) { if (dependency.isOptional()) { log.debug("Enable check for extension dependencies via ASM."); checkForExtensionDependencies = true; break; } } } } /** * Returns true, if the extension finder checks extensions for its required plugins. * This feature has to be enabled, in order check the availability of * {@link Extension#plugins()} configured by an extension. *

* This feature is enabled by default, if at least one available plugin makes use of * optional plugin dependencies. Those optional plugins might not be available at runtime. * Therefore any extension is checked by default against available plugins before its * instantiation. *

* Notice: This feature requires the optional ASM library * to be available on the applications classpath. * * @return true, if the extension finder checks extensions for its required plugins */ public final boolean isCheckForExtensionDependencies() { return Boolean.TRUE.equals(checkForExtensionDependencies); } /** * Plugin developers may enable / disable checks for required plugins of an extension. * This feature has to be enabled, in order check the availability of * {@link Extension#plugins()} configured by an extension. *

* This feature is enabled by default, if at least one available plugin makes use of * optional plugin dependencies. Those optional plugins might not be available at runtime. * Therefore any extension is checked by default against available plugins before its * instantiation. *

* Notice: This feature requires the optional ASM library * to be available on the applications classpath. * * @param checkForExtensionDependencies true to enable checks for optional extensions, otherwise false */ public void setCheckForExtensionDependencies(boolean checkForExtensionDependencies) { this.checkForExtensionDependencies = checkForExtensionDependencies; } protected void debugExtensions(Set extensions) { if (log.isDebugEnabled()) { if (extensions.isEmpty()) { log.debug("No extensions found"); } else { log.debug("Found possible {} extensions:", extensions.size()); for (String extension : extensions) { log.debug(" " + extension); } } } } private Map> readStorages() { Map> result = new LinkedHashMap<>(); result.putAll(readClasspathStorages()); result.putAll(readPluginsStorages()); return result; } private Map> getEntries() { if (entries == null) { entries = readStorages(); } return entries; } /** * Returns the parameters of an {@link Extension} annotation without loading * the corresponding class into the class loader. * * @param className name of the class, that holds the requested {@link Extension} annotation * @param classLoader class loader to access the class * @return the contents of the {@link Extension} annotation or null, if the class does not * have an {@link Extension} annotation */ private ExtensionInfo getExtensionInfo(String className, ClassLoader classLoader) { if (extensionInfos == null) { extensionInfos = new HashMap<>(); } if (!extensionInfos.containsKey(className)) { log.trace("Load annotation for '{}' using asm", className); ExtensionInfo info = ExtensionInfo.load(className, classLoader); if (info == null) { log.warn("No extension annotation was found for '{}'", className); extensionInfos.put(className, null); } else { extensionInfos.put(className, info); } } return extensionInfos.get(className); } private ExtensionWrapper createExtensionWrapper(Class extensionClass) { Extension extensionAnnotation = findExtensionAnnotation(extensionClass); int ordinal = extensionAnnotation != null ? extensionAnnotation.ordinal() : 0; ExtensionDescriptor descriptor = new ExtensionDescriptor(ordinal, extensionClass); return new ExtensionWrapper<>(descriptor, pluginManager.getExtensionFactory()); } public static Extension findExtensionAnnotation(Class clazz) { if (clazz.isAnnotationPresent(Extension.class)) { return clazz.getAnnotation(Extension.class); } // search recursively through all annotations for (Annotation annotation : clazz.getAnnotations()) { Class annotationClass = annotation.annotationType(); if (!annotationClass.getName().startsWith("java.lang.annotation")) { Extension extensionAnnotation = findExtensionAnnotation(annotationClass); if (extensionAnnotation != null) { return extensionAnnotation; } } } return null; } private void checkDifferentClassLoaders(Class type, Class extensionClass) { ClassLoader typeClassLoader = type.getClassLoader(); // class loader of extension point ClassLoader extensionClassLoader = extensionClass.getClassLoader(); boolean match = ClassUtils.getAllInterfacesNames(extensionClass).contains(type.getSimpleName()); if (match && !extensionClassLoader.equals(typeClassLoader)) { // in this scenario the method 'isAssignableFrom' returns only FALSE // see http://www.coderanch.com/t/557846/java/java/FWIW-FYI-isAssignableFrom-isInstance-differing log.error("Different class loaders: '{}' (E) and '{}' (EP)", extensionClassLoader, typeClassLoader); } } }