@@ -43,6 +43,12 @@ | |||
<artifactId>java-semver</artifactId> | |||
<version>0.9.0</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.ow2.asm</groupId> | |||
<artifactId>asm</artifactId> | |||
<version>${asm.version}</version> | |||
<optional>true</optional> | |||
</dependency> | |||
<dependency> | |||
<groupId>junit</groupId> | |||
<artifactId>junit</artifactId> |
@@ -15,12 +15,14 @@ | |||
*/ | |||
package org.pf4j; | |||
import org.pf4j.asm.ExtensionInfo; | |||
import org.pf4j.util.ClassUtils; | |||
import org.slf4j.Logger; | |||
import org.slf4j.LoggerFactory; | |||
import java.util.ArrayList; | |||
import java.util.Collections; | |||
import java.util.HashMap; | |||
import java.util.LinkedHashMap; | |||
import java.util.List; | |||
import java.util.Map; | |||
@@ -35,6 +37,8 @@ public abstract class AbstractExtensionFinder implements ExtensionFinder, Plugin | |||
protected PluginManager pluginManager; | |||
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) { | |||
this.pluginManager = pluginManager; | |||
@@ -97,6 +101,41 @@ public abstract class AbstractExtensionFinder implements ExtensionFinder, Plugin | |||
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<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); | |||
Class<?> extensionClass = classLoader.loadClass(className); | |||
@@ -186,6 +225,60 @@ public abstract class AbstractExtensionFinder implements ExtensionFinder, Plugin | |||
// 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. | |||
* <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) { | |||
@@ -218,6 +311,34 @@ public abstract class AbstractExtensionFinder implements ExtensionFinder, Plugin | |||
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) { | |||
int ordinal = 0; | |||
if (extensionClass.isAnnotationPresent(Extension.class)) { |
@@ -143,9 +143,23 @@ public class DependencyResolver { | |||
dependenciesGraph.addVertex(pluginId); | |||
dependentsGraph.addVertex(pluginId); | |||
} else { | |||
boolean edgeAdded = false; | |||
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); | |||
} | |||
} | |||
} |
@@ -47,4 +47,16 @@ public @interface Extension { | |||
*/ | |||
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 {}; | |||
} |
@@ -194,6 +194,12 @@ public class PluginClassLoader extends URLClassLoader { | |||
List<PluginDependency> dependencies = pluginDescriptor.getDependencies(); | |||
for (PluginDependency dependency : dependencies) { | |||
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 { | |||
return classLoader.loadClass(className); | |||
} catch (ClassNotFoundException e) { |
@@ -22,6 +22,7 @@ public class PluginDependency { | |||
private String pluginId; | |||
private String pluginVersionSupport = "*"; | |||
private boolean optional; | |||
public PluginDependency(String dependency) { | |||
int index = dependency.indexOf('@'); | |||
@@ -33,6 +34,13 @@ public class PluginDependency { | |||
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() { | |||
@@ -43,9 +51,15 @@ public class PluginDependency { | |||
return pluginVersionSupport; | |||
} | |||
public boolean isOptional() { | |||
return optional; | |||
} | |||
@Override | |||
public String toString() { | |||
return "PluginDependency [pluginId=" + pluginId + ", pluginVersionSupport=" + pluginVersionSupport + "]"; | |||
return "PluginDependency [pluginId=" + pluginId + ", pluginVersionSupport=" | |||
+ pluginVersionSupport + ", optional=" | |||
+ optional + "]"; | |||
} | |||
} |
@@ -0,0 +1,102 @@ | |||
/* | |||
* 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; | |||
} | |||
} | |||
} |
@@ -0,0 +1,96 @@ | |||
/* | |||
* 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); | |||
} | |||
}; | |||
} | |||
} |
@@ -32,15 +32,34 @@ public class PluginDependencyTest { | |||
PluginDependency instance = new PluginDependency("test"); | |||
assertEquals("test", instance.getPluginId()); | |||
assertEquals("*", instance.getPluginVersionSupport()); | |||
assertEquals(false, instance.isOptional()); | |||
instance = new PluginDependency("test@"); | |||
assertEquals("test", instance.getPluginId()); | |||
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"); | |||
assertEquals("test", instance.getPluginId()); | |||
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()); | |||
} | |||
} |
@@ -45,6 +45,7 @@ | |||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | |||
<java.version>1.7</java.version> | |||
<slf4j.version>1.7.25</slf4j.version> | |||
<asm.version>7.0</asm.version> | |||
<junit.version>4.12</junit.version> | |||
<mockito.version>2.0.28-beta</mockito.version> |