/*
* 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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.pf4j.util.DirectedGraph;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* This class builds a dependency graph for a list of plugins (descriptors).
* The entry point is the {@link #resolve(List)} method, method that returns a {@link Result} object.
* The {@code Result} class contains nice information about the result of resolve operation (if it's a cyclic dependency,
* they are not found dependencies, they are dependencies with wrong version).
* This class is very useful for if-else scenarios.
*
* Only some attributes (pluginId, dependencies and pluginVersion) from {@link PluginDescriptor} are used in
* the process of {@code resolve} operation.
*
* @author Decebal Suiu
*/
public class DependencyResolver {
private static final Logger log = LoggerFactory.getLogger(DependencyResolver.class);
private final VersionManager versionManager;
private DirectedGraph dependenciesGraph; // the value is 'pluginId'
private DirectedGraph dependentsGraph; // the value is 'pluginId'
private boolean resolved;
public DependencyResolver(VersionManager versionManager) {
this.versionManager = versionManager;
}
/**
* Resolve the dependencies for the given plugins.
*
* @param plugins the list of plugins
* @return a {@link Result} object
*/
public Result resolve(List plugins) {
// create graphs
dependenciesGraph = new DirectedGraph<>();
dependentsGraph = new DirectedGraph<>();
// populate graphs
Map pluginByIds = new HashMap<>();
for (PluginDescriptor plugin : plugins) {
addPlugin(plugin);
pluginByIds.put(plugin.getPluginId(), plugin);
}
log.debug("Graph: {}", dependenciesGraph);
// get a sorted list of dependencies
List sortedPlugins = dependenciesGraph.reverseTopologicalSort();
log.debug("Plugins order: {}", sortedPlugins);
// create the result object
Result result = new Result(sortedPlugins);
resolved = true;
if (sortedPlugins != null) { // no cyclic dependency
// detect not found dependencies
for (String pluginId : sortedPlugins) {
if (!pluginByIds.containsKey(pluginId)) {
result.addNotFoundDependency(pluginId);
}
}
}
// check dependencies versions
for (PluginDescriptor plugin : plugins) {
String pluginId = plugin.getPluginId();
String existingVersion = plugin.getVersion();
List dependents = getDependents(pluginId);
while (!dependents.isEmpty()) {
String dependentId = dependents.remove(0);
PluginDescriptor dependent = pluginByIds.get(dependentId);
String requiredVersion = getDependencyVersionSupport(dependent, pluginId);
boolean ok = checkDependencyVersion(requiredVersion, existingVersion);
if (!ok) {
result.addWrongDependencyVersion(new WrongDependencyVersion(pluginId, dependentId, existingVersion, requiredVersion));
}
}
}
return result;
}
/**
* Retrieves the plugins ids that the given plugin id directly depends on.
*
* @param pluginId the unique plugin identifier, specified in its metadata
* @return an immutable list of dependencies (new list for each call)
*/
public List getDependencies(String pluginId) {
checkResolved();
return new ArrayList<>(dependenciesGraph.getNeighbors(pluginId));
}
/**
* Retrieves the plugins ids that the given content is a direct dependency of.
*
* @param pluginId the unique plugin identifier, specified in its metadata
* @return an immutable list of dependents (new list for each call)
*/
public List getDependents(String pluginId) {
checkResolved();
return new ArrayList<>(dependentsGraph.getNeighbors(pluginId));
}
/**
* Check if an existing version of dependency is compatible with the required version (from plugin descriptor).
*
* @param requiredVersion the required version
* @param existingVersion the existing version
* @return {@code true} if the existing version is compatible with the required version, {@code false} otherwise
*/
protected boolean checkDependencyVersion(String requiredVersion, String existingVersion) {
return versionManager.checkVersionConstraint(existingVersion, requiredVersion);
}
private void addPlugin(PluginDescriptor descriptor) {
String pluginId = descriptor.getPluginId();
List dependencies = descriptor.getDependencies();
if (dependencies.isEmpty()) {
dependenciesGraph.addVertex(pluginId);
dependentsGraph.addVertex(pluginId);
} else {
boolean edgeAdded = false;
for (PluginDependency dependency : dependencies) {
// 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);
}
}
}
private void checkResolved() {
if (!resolved) {
throw new IllegalStateException("Call 'resolve' method first");
}
}
private String getDependencyVersionSupport(PluginDescriptor dependent, String dependencyId) {
List dependencies = dependent.getDependencies();
for (PluginDependency dependency : dependencies) {
if (dependencyId.equals(dependency.getPluginId())) {
return dependency.getPluginVersionSupport();
}
}
throw new IllegalStateException("Cannot find a dependency with id '" + dependencyId +
"' for plugin '" + dependent.getPluginId() + "'");
}
/**
* The result of the {@link #resolve(List)} operation.
*/
public static class Result {
private boolean cyclicDependency;
private final List notFoundDependencies; // value is "pluginId"
private final List sortedPlugins; // value is "pluginId"
private final List wrongVersionDependencies;
Result(List sortedPlugins) {
if (sortedPlugins == null) {
cyclicDependency = true;
this.sortedPlugins = Collections.emptyList();
} else {
this.sortedPlugins = new ArrayList<>(sortedPlugins);
}
notFoundDependencies = new ArrayList<>();
wrongVersionDependencies = new ArrayList<>();
}
/**
* Returns true is a cyclic dependency was detected.
*
* @return true is a cyclic dependency was detected
*/
public boolean hasCyclicDependency() {
return cyclicDependency;
}
/**
* Returns a list with dependencies required that were not found.
*
* @return a list with dependencies required that were not found
*/
public List getNotFoundDependencies() {
return notFoundDependencies;
}
/**
* Returns a list with dependencies with wrong version.
*
* @return a list with dependencies with wrong version
*/
public List getWrongVersionDependencies() {
return wrongVersionDependencies;
}
/**
* Get the list of plugins in dependency sorted order.
*
* @return the list of plugins in dependency sorted order
*/
public List getSortedPlugins() {
return sortedPlugins;
}
void addNotFoundDependency(String pluginId) {
notFoundDependencies.add(pluginId);
}
void addWrongDependencyVersion(WrongDependencyVersion wrongDependencyVersion) {
wrongVersionDependencies.add(wrongDependencyVersion);
}
}
/**
* Represents a wrong dependency version.
*/
public static class WrongDependencyVersion {
private final String dependencyId; // value is "pluginId"
private final String dependentId; // value is "pluginId"
private final String existingVersion;
private final String requiredVersion;
WrongDependencyVersion(String dependencyId, String dependentId, String existingVersion, String requiredVersion) {
this.dependencyId = dependencyId;
this.dependentId = dependentId;
this.existingVersion = existingVersion;
this.requiredVersion = requiredVersion;
}
public String getDependencyId() {
return dependencyId;
}
public String getDependentId() {
return dependentId;
}
public String getExistingVersion() {
return existingVersion;
}
public String getRequiredVersion() {
return requiredVersion;
}
@Override
public String toString() {
return "WrongDependencyVersion{" +
"dependencyId='" + dependencyId + '\'' +
", dependentId='" + dependentId + '\'' +
", existingVersion='" + existingVersion + '\'' +
", requiredVersion='" + requiredVersion + '\'' +
'}';
}
}
/**
* It will be thrown if a cyclic dependency is detected.
*/
public static class CyclicDependencyException extends PluginRuntimeException {
public CyclicDependencyException() {
super("Cyclic dependencies");
}
}
/**
* Indicates that the dependencies required were not found.
*/
public static class DependenciesNotFoundException extends PluginRuntimeException {
private final List dependencies;
public DependenciesNotFoundException(List dependencies) {
super("Dependencies '{}' not found", dependencies);
this.dependencies = dependencies;
}
public List getDependencies() {
return dependencies;
}
}
/**
* Indicates that some dependencies have wrong version.
*/
public static class DependenciesWrongVersionException extends PluginRuntimeException {
private List dependencies;
public DependenciesWrongVersionException(List dependencies) {
super("Dependencies '{}' have wrong version", dependencies);
this.dependencies = dependencies;
}
public List getDependencies() {
return dependencies;
}
}
}