<artifactId>java-semver</artifactId> | <artifactId>java-semver</artifactId> | ||||
<version>0.9.0</version> | <version>0.9.0</version> | ||||
</dependency> | </dependency> | ||||
<dependency> | |||||
<groupId>org.ow2.asm</groupId> | |||||
<artifactId>asm</artifactId> | |||||
<version>${asm.version}</version> | |||||
<optional>true</optional> | |||||
</dependency> | |||||
<dependency> | <dependency> | ||||
<groupId>junit</groupId> | <groupId>junit</groupId> | ||||
<artifactId>junit</artifactId> | <artifactId>junit</artifactId> |
*/ | */ | ||||
package org.pf4j; | package org.pf4j; | ||||
import org.pf4j.asm.ExtensionInfo; | |||||
import org.pf4j.util.ClassUtils; | import org.pf4j.util.ClassUtils; | ||||
import org.slf4j.Logger; | import org.slf4j.Logger; | ||||
import org.slf4j.LoggerFactory; | import org.slf4j.LoggerFactory; | ||||
import java.util.ArrayList; | import java.util.ArrayList; | ||||
import java.util.Collections; | import java.util.Collections; | ||||
import java.util.HashMap; | |||||
import java.util.LinkedHashMap; | import java.util.LinkedHashMap; | ||||
import java.util.List; | import java.util.List; | ||||
import java.util.Map; | import java.util.Map; | ||||
protected PluginManager pluginManager; | protected PluginManager pluginManager; | ||||
protected volatile Map<String, Set<String>> entries; // cache by pluginId | protected volatile Map<String, Set<String>> entries; // cache by pluginId | ||||
protected volatile Map<String, ExtensionInfo> extensionInfos; // cache extension infos by class name | |||||
protected Boolean checkForExtensionDependencies = null; | |||||
public AbstractExtensionFinder(PluginManager pluginManager) { | public AbstractExtensionFinder(PluginManager pluginManager) { | ||||
this.pluginManager = pluginManager; | this.pluginManager = pluginManager; | ||||
for (String className : classNames) { | for (String className : classNames) { | ||||
try { | 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<String> 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); | log.debug("Loading class '{}' using class loader '{}'", className, classLoader); | ||||
Class<?> extensionClass = classLoader.loadClass(className); | Class<?> extensionClass = classLoader.loadClass(className); | ||||
// TODO optimize (do only for some transitions) | // TODO optimize (do only for some transitions) | ||||
// clear cache | // clear cache | ||||
entries = null; | 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. | |||||
* <p> | |||||
* 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. | |||||
* <p> | |||||
* Notice: This feature requires the optional <a href="https://asm.ow2.io/">ASM library</a> | |||||
* 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. | |||||
* <p> | |||||
* 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. | |||||
* <p> | |||||
* Notice: This feature requires the optional <a href="https://asm.ow2.io/">ASM library</a> | |||||
* 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<String> extensions) { | protected void debugExtensions(Set<String> extensions) { | ||||
return entries; | 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) { | private ExtensionWrapper createExtensionWrapper(Class<?> extensionClass) { | ||||
int ordinal = 0; | int ordinal = 0; | ||||
if (extensionClass.isAnnotationPresent(Extension.class)) { | if (extensionClass.isAnnotationPresent(Extension.class)) { |
dependenciesGraph.addVertex(pluginId); | dependenciesGraph.addVertex(pluginId); | ||||
dependentsGraph.addVertex(pluginId); | dependentsGraph.addVertex(pluginId); | ||||
} else { | } else { | ||||
boolean edgeAdded = false; | |||||
for (PluginDependency dependency : dependencies) { | for (PluginDependency dependency : dependencies) { | ||||
dependenciesGraph.addEdge(pluginId, dependency.getPluginId()); | |||||
dependentsGraph.addEdge(dependency.getPluginId(), pluginId); | |||||
// Don't register optional plugins in the dependency graph | |||||
// to avoid automatic disabling of the plugin, | |||||
// if an optional dependency is missing. | |||||
if (!dependency.isOptional()) { | |||||
edgeAdded = true; | |||||
dependenciesGraph.addEdge(pluginId, dependency.getPluginId()); | |||||
dependentsGraph.addEdge(dependency.getPluginId(), pluginId); | |||||
} | |||||
} | |||||
// Register the plugin without dependencies, | |||||
// if all of its dependencies are optional. | |||||
if (!edgeAdded) { | |||||
dependenciesGraph.addVertex(pluginId); | |||||
dependentsGraph.addVertex(pluginId); | |||||
} | } | ||||
} | } | ||||
} | } |
*/ | */ | ||||
Class<? extends ExtensionPoint>[] points() default {}; | Class<? extends ExtensionPoint>[] points() default {}; | ||||
/** | |||||
* An array of plugin IDs, that have to be available in order to load this extension. | |||||
* The {@link AbstractExtensionFinder} won't load this extension, if these plugins are not | |||||
* available / started at runtime. | |||||
* <p> | |||||
* Notice: This feature requires the optional <a href="https://asm.ow2.io/">ASM library</a> | |||||
* to be available on the applications classpath and has to be explicitly enabled via | |||||
* {@link AbstractExtensionFinder#setCheckForExtensionDependencies(boolean)}. | |||||
* | |||||
* @return plugin IDs, that have to be available in order to load this extension | |||||
*/ | |||||
String[] plugins() default {}; | |||||
} | } |
List<PluginDependency> dependencies = pluginDescriptor.getDependencies(); | List<PluginDependency> dependencies = pluginDescriptor.getDependencies(); | ||||
for (PluginDependency dependency : dependencies) { | for (PluginDependency dependency : dependencies) { | ||||
ClassLoader classLoader = pluginManager.getPluginClassLoader(dependency.getPluginId()); | ClassLoader classLoader = pluginManager.getPluginClassLoader(dependency.getPluginId()); | ||||
// If the dependency is marked as optional, its class loader might not be available. | |||||
if (classLoader == null && dependency.isOptional()) { | |||||
continue; | |||||
} | |||||
try { | try { | ||||
return classLoader.loadClass(className); | return classLoader.loadClass(className); | ||||
} catch (ClassNotFoundException e) { | } catch (ClassNotFoundException e) { |
private String pluginId; | private String pluginId; | ||||
private String pluginVersionSupport = "*"; | private String pluginVersionSupport = "*"; | ||||
private boolean optional; | |||||
public PluginDependency(String dependency) { | public PluginDependency(String dependency) { | ||||
int index = dependency.indexOf('@'); | int index = dependency.indexOf('@'); | ||||
this.pluginVersionSupport = dependency.substring(index + 1); | this.pluginVersionSupport = dependency.substring(index + 1); | ||||
} | } | ||||
} | } | ||||
// A dependency is considered as optional, | |||||
// if the plugin id ends with a question mark. | |||||
this.optional = this.pluginId.endsWith("?"); | |||||
if (this.optional) { | |||||
this.pluginId = this.pluginId.substring(0, this.pluginId.length() - 1); | |||||
} | |||||
} | } | ||||
public String getPluginId() { | public String getPluginId() { | ||||
return pluginVersionSupport; | return pluginVersionSupport; | ||||
} | } | ||||
public boolean isOptional() { | |||||
return optional; | |||||
} | |||||
@Override | @Override | ||||
public String toString() { | public String toString() { | ||||
return "PluginDependency [pluginId=" + pluginId + ", pluginVersionSupport=" + pluginVersionSupport + "]"; | |||||
return "PluginDependency [pluginId=" + pluginId + ", pluginVersionSupport=" | |||||
+ pluginVersionSupport + ", optional=" | |||||
+ optional + "]"; | |||||
} | } | ||||
} | } |
/* | |||||
* 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.asm; | |||||
import org.objectweb.asm.ClassReader; | |||||
import org.pf4j.Extension; | |||||
import org.slf4j.Logger; | |||||
import org.slf4j.LoggerFactory; | |||||
import java.io.IOException; | |||||
import java.io.InputStream; | |||||
import java.util.ArrayList; | |||||
import java.util.Collections; | |||||
import java.util.List; | |||||
/** | |||||
* This class holds the parameters of an {@link org.pf4j.Extension} | |||||
* annotation defined for a certain class. | |||||
* | |||||
* @author Andreas Rudolph | |||||
* @author Decebal Suiu | |||||
*/ | |||||
public final class ExtensionInfo { | |||||
private static final Logger log = LoggerFactory.getLogger(ExtensionInfo.class); | |||||
private final String className; | |||||
int ordinal = 0; | |||||
List<String> plugins = new ArrayList<>(); | |||||
List<String> points = new ArrayList<>(); | |||||
private ExtensionInfo(String className) { | |||||
super(); | |||||
this.className = className; | |||||
} | |||||
/** | |||||
* Get the name of the class, for which extension info was created. | |||||
* | |||||
* @return absolute class name | |||||
*/ | |||||
public String getClassName() { | |||||
return className; | |||||
} | |||||
/** | |||||
* Get the {@link Extension#ordinal()} value, that was assigned to the extension. | |||||
* | |||||
* @return ordinal value | |||||
*/ | |||||
public int getOrdinal() { | |||||
return ordinal; | |||||
} | |||||
/** | |||||
* Get the {@link Extension#plugins()} value, that was assigned to the extension. | |||||
* | |||||
* @return ordinal value | |||||
*/ | |||||
public List<String> getPlugins() { | |||||
return Collections.unmodifiableList(plugins); | |||||
} | |||||
/** | |||||
* Get the {@link Extension#points()} value, that was assigned to the extension. | |||||
* | |||||
* @return ordinal value | |||||
*/ | |||||
public List<String> getPoints() { | |||||
return Collections.unmodifiableList(points); | |||||
} | |||||
/** | |||||
* Load an {@link ExtensionInfo} for a certain class. | |||||
* | |||||
* @param className absolute class name | |||||
* @param classLoader class loader to access the class | |||||
* @return the {@link ExtensionInfo}, if the class was annotated with an {@link Extension}, otherwise null | |||||
*/ | |||||
public static ExtensionInfo load(String className, ClassLoader classLoader) { | |||||
try (InputStream input = classLoader.getResourceAsStream(className.replace('.', '/') + ".class")) { | |||||
ExtensionInfo info = new ExtensionInfo(className); | |||||
new ClassReader(input).accept(new ExtensionVisitor(info), ClassReader.SKIP_DEBUG); | |||||
return info; | |||||
} catch (IOException e) { | |||||
log.error(e.getMessage(), e); | |||||
return null; | |||||
} | |||||
} | |||||
} |
/* | |||||
* 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.asm; | |||||
import org.objectweb.asm.AnnotationVisitor; | |||||
import org.objectweb.asm.ClassVisitor; | |||||
import org.objectweb.asm.Opcodes; | |||||
import org.objectweb.asm.Type; | |||||
import org.pf4j.Extension; | |||||
import java.util.Arrays; | |||||
/** | |||||
* This visitor extracts an {@link ExtensionInfo} from any class, | |||||
* that holds an {@link Extension} annotation. | |||||
* <p> | |||||
* The annotation parameters are extracted from byte code by using the | |||||
* <a href="https://asm.ow2.io/">ASM library</a>. This makes it possible to | |||||
* access the {@link Extension} parameters without loading the class into | |||||
* the class loader. This avoids possible {@link NoClassDefFoundError}'s | |||||
* for extensions, that can't be loaded due to missing dependencies. | |||||
* | |||||
* @author Andreas Rudolph | |||||
* @author Decebal Suiu | |||||
*/ | |||||
class ExtensionVisitor extends ClassVisitor { | |||||
//private static final Logger log = LoggerFactory.getLogger(ExtensionVisitor.class); | |||||
private static final int ASM_VERSION = Opcodes.ASM7; | |||||
private final ExtensionInfo extensionInfo; | |||||
ExtensionVisitor(ExtensionInfo extensionInfo) { | |||||
super(ASM_VERSION); | |||||
this.extensionInfo = extensionInfo; | |||||
} | |||||
@Override | |||||
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { | |||||
//if (!descriptor.equals("Lorg/pf4j/Extension;")) { | |||||
if (!Type.getType(descriptor).getClassName().equals(Extension.class.getName())) { | |||||
return super.visitAnnotation(descriptor, visible); | |||||
} | |||||
return new AnnotationVisitor(ASM_VERSION) { | |||||
@Override | |||||
public AnnotationVisitor visitArray(final String name) { | |||||
if ("ordinal".equals(name) || "plugins".equals(name) || "points".equals(name)) { | |||||
return new AnnotationVisitor(ASM_VERSION, super.visitArray(name)) { | |||||
@Override | |||||
public void visit(String key, Object value) { | |||||
//log.debug("Load annotation attribute {} = {} ({})", name, value, value.getClass().getName()); | |||||
if ("ordinal".equals(name)) { | |||||
extensionInfo.ordinal = Integer.parseInt(value.toString()); | |||||
} else if ("plugins".equals(name)) { | |||||
if (value instanceof String) { | |||||
//log.debug("found plugin " + value); | |||||
extensionInfo.plugins.add((String) value); | |||||
} else if (value instanceof String[]) { | |||||
//log.debug("found plugins " + Arrays.toString((String[]) value)); | |||||
extensionInfo.plugins.addAll(Arrays.asList((String[]) value)); | |||||
} else { | |||||
//log.debug("found plugin " + value.toString()); | |||||
extensionInfo.plugins.add(value.toString()); | |||||
} | |||||
} else if ("points".equals(name)) { | |||||
String pointClassName = ((Type) value).getClassName(); | |||||
//log.debug("found point " + pointClassName); | |||||
extensionInfo.points.add(pointClassName); | |||||
} | |||||
super.visit(key, value); | |||||
} | |||||
}; | |||||
} | |||||
return super.visitArray(name); | |||||
} | |||||
}; | |||||
} | |||||
} |
PluginDependency instance = new PluginDependency("test"); | PluginDependency instance = new PluginDependency("test"); | ||||
assertEquals("test", instance.getPluginId()); | assertEquals("test", instance.getPluginId()); | ||||
assertEquals("*", instance.getPluginVersionSupport()); | assertEquals("*", instance.getPluginVersionSupport()); | ||||
assertEquals(false, instance.isOptional()); | |||||
instance = new PluginDependency("test@"); | instance = new PluginDependency("test@"); | ||||
assertEquals("test", instance.getPluginId()); | assertEquals("test", instance.getPluginId()); | ||||
assertEquals("*", instance.getPluginVersionSupport()); | assertEquals("*", instance.getPluginVersionSupport()); | ||||
assertEquals(false, instance.isOptional()); | |||||
instance = new PluginDependency("test?"); | |||||
assertEquals("test", instance.getPluginId()); | |||||
assertEquals("*", instance.getPluginVersionSupport()); | |||||
assertEquals(true, instance.isOptional()); | |||||
instance = new PluginDependency("test?@"); | |||||
assertEquals("test", instance.getPluginId()); | |||||
assertEquals("*", instance.getPluginVersionSupport()); | |||||
assertEquals(true, instance.isOptional()); | |||||
instance = new PluginDependency("test@1.0"); | instance = new PluginDependency("test@1.0"); | ||||
assertEquals("test", instance.getPluginId()); | assertEquals("test", instance.getPluginId()); | ||||
assertEquals("1.0", instance.getPluginVersionSupport()); | assertEquals("1.0", instance.getPluginVersionSupport()); | ||||
assertEquals("PluginDependency [pluginId=test, pluginVersionSupport=1.0]", instance.toString()); | |||||
assertEquals(false, instance.isOptional()); | |||||
assertEquals("PluginDependency [pluginId=test, pluginVersionSupport=1.0, optional=false]", instance.toString()); | |||||
instance = new PluginDependency("test?@1.0"); | |||||
assertEquals("test", instance.getPluginId()); | |||||
assertEquals("1.0", instance.getPluginVersionSupport()); | |||||
assertEquals(true, instance.isOptional()); | |||||
assertEquals("PluginDependency [pluginId=test, pluginVersionSupport=1.0, optional=true]", instance.toString()); | |||||
} | } | ||||
} | } |
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | ||||
<java.version>1.7</java.version> | <java.version>1.7</java.version> | ||||
<slf4j.version>1.7.25</slf4j.version> | <slf4j.version>1.7.25</slf4j.version> | ||||
<asm.version>7.0</asm.version> | |||||
<junit.version>4.12</junit.version> | <junit.version>4.12</junit.version> | ||||
<mockito.version>2.0.28-beta</mockito.version> | <mockito.version>2.0.28-beta</mockito.version> |