<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>
*/
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;
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;
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);
// 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) {
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)) {
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);
}
}
}
*/
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();
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) {
private String pluginId;
private String pluginVersionSupport = "*";
+ private boolean optional;
public PluginDependency(String dependency) {
int index = dependency.indexOf('@');
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() {
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 + "]";
}
}
--- /dev/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.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;
+ }
+ }
+}
--- /dev/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");
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());
}
}
<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>