You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

DependencyResolver.java 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. /*
  2. * Copyright (C) 2012-present the original author or authors.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. package org.pf4j;
  17. import org.slf4j.Logger;
  18. import org.slf4j.LoggerFactory;
  19. import org.pf4j.util.DirectedGraph;
  20. import java.util.ArrayList;
  21. import java.util.Collections;
  22. import java.util.HashMap;
  23. import java.util.List;
  24. import java.util.Map;
  25. /**
  26. * This class builds a dependency graph for a list of plugins (descriptors).
  27. * The entry point is the {@link #resolve(List)} method, method that returns a {@link Result} object.
  28. * The {@code Result} class contains nice information about the result of resolve operation (if it's a cyclic dependency,
  29. * they are not found dependencies, they are dependencies with wrong version).
  30. * This class is very useful for if-else scenarios.
  31. * <p>
  32. * Only some attributes (pluginId, dependencies and pluginVersion) from {@link PluginDescriptor} are used in
  33. * the process of {@code resolve} operation.
  34. *
  35. * @author Decebal Suiu
  36. */
  37. public class DependencyResolver {
  38. private static final Logger log = LoggerFactory.getLogger(DependencyResolver.class);
  39. private final VersionManager versionManager;
  40. private DirectedGraph<String> dependenciesGraph; // the value is 'pluginId'
  41. private DirectedGraph<String> dependentsGraph; // the value is 'pluginId'
  42. private boolean resolved;
  43. public DependencyResolver(VersionManager versionManager) {
  44. this.versionManager = versionManager;
  45. }
  46. /**
  47. * Resolve the dependencies for the given plugins.
  48. *
  49. * @param plugins the list of plugins
  50. * @return a {@link Result} object
  51. */
  52. public Result resolve(List<PluginDescriptor> plugins) {
  53. // create graphs
  54. dependenciesGraph = new DirectedGraph<>();
  55. dependentsGraph = new DirectedGraph<>();
  56. // populate graphs
  57. Map<String, PluginDescriptor> pluginByIds = new HashMap<>();
  58. for (PluginDescriptor plugin : plugins) {
  59. addPlugin(plugin);
  60. pluginByIds.put(plugin.getPluginId(), plugin);
  61. }
  62. log.debug("Graph: {}", dependenciesGraph);
  63. // get a sorted list of dependencies
  64. List<String> sortedPlugins = dependenciesGraph.reverseTopologicalSort();
  65. log.debug("Plugins order: {}", sortedPlugins);
  66. // create the result object
  67. Result result = new Result(sortedPlugins);
  68. resolved = true;
  69. if (sortedPlugins != null) { // no cyclic dependency
  70. // detect not found dependencies
  71. for (String pluginId : sortedPlugins) {
  72. if (!pluginByIds.containsKey(pluginId)) {
  73. result.addNotFoundDependency(pluginId);
  74. }
  75. }
  76. }
  77. // check dependencies versions
  78. for (PluginDescriptor plugin : plugins) {
  79. String pluginId = plugin.getPluginId();
  80. String existingVersion = plugin.getVersion();
  81. List<String> dependents = getDependents(pluginId);
  82. while (!dependents.isEmpty()) {
  83. String dependentId = dependents.remove(0);
  84. PluginDescriptor dependent = pluginByIds.get(dependentId);
  85. String requiredVersion = getDependencyVersionSupport(dependent, pluginId);
  86. boolean ok = checkDependencyVersion(requiredVersion, existingVersion);
  87. if (!ok) {
  88. result.addWrongDependencyVersion(new WrongDependencyVersion(pluginId, dependentId, existingVersion, requiredVersion));
  89. }
  90. }
  91. }
  92. return result;
  93. }
  94. /**
  95. * Retrieves the plugins ids that the given plugin id directly depends on.
  96. *
  97. * @param pluginId the unique plugin identifier, specified in its metadata
  98. * @return an immutable list of dependencies (new list for each call)
  99. */
  100. public List<String> getDependencies(String pluginId) {
  101. checkResolved();
  102. return new ArrayList<>(dependenciesGraph.getNeighbors(pluginId));
  103. }
  104. /**
  105. * Retrieves the plugins ids that the given content is a direct dependency of.
  106. *
  107. * @param pluginId the unique plugin identifier, specified in its metadata
  108. * @return an immutable list of dependents (new list for each call)
  109. */
  110. public List<String> getDependents(String pluginId) {
  111. checkResolved();
  112. return new ArrayList<>(dependentsGraph.getNeighbors(pluginId));
  113. }
  114. /**
  115. * Check if an existing version of dependency is compatible with the required version (from plugin descriptor).
  116. *
  117. * @param requiredVersion the required version
  118. * @param existingVersion the existing version
  119. * @return {@code true} if the existing version is compatible with the required version, {@code false} otherwise
  120. */
  121. protected boolean checkDependencyVersion(String requiredVersion, String existingVersion) {
  122. return versionManager.checkVersionConstraint(existingVersion, requiredVersion);
  123. }
  124. private void addPlugin(PluginDescriptor descriptor) {
  125. String pluginId = descriptor.getPluginId();
  126. List<PluginDependency> dependencies = descriptor.getDependencies();
  127. if (dependencies.isEmpty()) {
  128. dependenciesGraph.addVertex(pluginId);
  129. dependentsGraph.addVertex(pluginId);
  130. } else {
  131. boolean edgeAdded = false;
  132. for (PluginDependency dependency : dependencies) {
  133. // Don't register optional plugins in the dependency graph to avoid automatic disabling of the plugin,
  134. // if an optional dependency is missing.
  135. if (!dependency.isOptional()) {
  136. edgeAdded = true;
  137. dependenciesGraph.addEdge(pluginId, dependency.getPluginId());
  138. dependentsGraph.addEdge(dependency.getPluginId(), pluginId);
  139. }
  140. }
  141. // Register the plugin without dependencies, if all of its dependencies are optional.
  142. if (!edgeAdded) {
  143. dependenciesGraph.addVertex(pluginId);
  144. dependentsGraph.addVertex(pluginId);
  145. }
  146. }
  147. }
  148. private void checkResolved() {
  149. if (!resolved) {
  150. throw new IllegalStateException("Call 'resolve' method first");
  151. }
  152. }
  153. private String getDependencyVersionSupport(PluginDescriptor dependent, String dependencyId) {
  154. List<PluginDependency> dependencies = dependent.getDependencies();
  155. for (PluginDependency dependency : dependencies) {
  156. if (dependencyId.equals(dependency.getPluginId())) {
  157. return dependency.getPluginVersionSupport();
  158. }
  159. }
  160. throw new IllegalStateException("Cannot find a dependency with id '" + dependencyId +
  161. "' for plugin '" + dependent.getPluginId() + "'");
  162. }
  163. /**
  164. * The result of the {@link #resolve(List)} operation.
  165. */
  166. public static class Result {
  167. private boolean cyclicDependency;
  168. private final List<String> notFoundDependencies; // value is "pluginId"
  169. private final List<String> sortedPlugins; // value is "pluginId"
  170. private final List<WrongDependencyVersion> wrongVersionDependencies;
  171. Result(List<String> sortedPlugins) {
  172. if (sortedPlugins == null) {
  173. cyclicDependency = true;
  174. this.sortedPlugins = Collections.emptyList();
  175. } else {
  176. this.sortedPlugins = new ArrayList<>(sortedPlugins);
  177. }
  178. notFoundDependencies = new ArrayList<>();
  179. wrongVersionDependencies = new ArrayList<>();
  180. }
  181. /**
  182. * Returns true is a cyclic dependency was detected.
  183. *
  184. * @return true is a cyclic dependency was detected
  185. */
  186. public boolean hasCyclicDependency() {
  187. return cyclicDependency;
  188. }
  189. /**
  190. * Returns a list with dependencies required that were not found.
  191. *
  192. * @return a list with dependencies required that were not found
  193. */
  194. public List<String> getNotFoundDependencies() {
  195. return notFoundDependencies;
  196. }
  197. /**
  198. * Returns a list with dependencies with wrong version.
  199. *
  200. * @return a list with dependencies with wrong version
  201. */
  202. public List<WrongDependencyVersion> getWrongVersionDependencies() {
  203. return wrongVersionDependencies;
  204. }
  205. /**
  206. * Get the list of plugins in dependency sorted order.
  207. *
  208. * @return the list of plugins in dependency sorted order
  209. */
  210. public List<String> getSortedPlugins() {
  211. return sortedPlugins;
  212. }
  213. void addNotFoundDependency(String pluginId) {
  214. notFoundDependencies.add(pluginId);
  215. }
  216. void addWrongDependencyVersion(WrongDependencyVersion wrongDependencyVersion) {
  217. wrongVersionDependencies.add(wrongDependencyVersion);
  218. }
  219. }
  220. /**
  221. * Represents a wrong dependency version.
  222. */
  223. public static class WrongDependencyVersion {
  224. private final String dependencyId; // value is "pluginId"
  225. private final String dependentId; // value is "pluginId"
  226. private final String existingVersion;
  227. private final String requiredVersion;
  228. WrongDependencyVersion(String dependencyId, String dependentId, String existingVersion, String requiredVersion) {
  229. this.dependencyId = dependencyId;
  230. this.dependentId = dependentId;
  231. this.existingVersion = existingVersion;
  232. this.requiredVersion = requiredVersion;
  233. }
  234. public String getDependencyId() {
  235. return dependencyId;
  236. }
  237. public String getDependentId() {
  238. return dependentId;
  239. }
  240. public String getExistingVersion() {
  241. return existingVersion;
  242. }
  243. public String getRequiredVersion() {
  244. return requiredVersion;
  245. }
  246. @Override
  247. public String toString() {
  248. return "WrongDependencyVersion{" +
  249. "dependencyId='" + dependencyId + '\'' +
  250. ", dependentId='" + dependentId + '\'' +
  251. ", existingVersion='" + existingVersion + '\'' +
  252. ", requiredVersion='" + requiredVersion + '\'' +
  253. '}';
  254. }
  255. }
  256. /**
  257. * It will be thrown if a cyclic dependency is detected.
  258. */
  259. public static class CyclicDependencyException extends PluginRuntimeException {
  260. public CyclicDependencyException() {
  261. super("Cyclic dependencies");
  262. }
  263. }
  264. /**
  265. * Indicates that the dependencies required were not found.
  266. */
  267. public static class DependenciesNotFoundException extends PluginRuntimeException {
  268. private final List<String> dependencies;
  269. public DependenciesNotFoundException(List<String> dependencies) {
  270. super("Dependencies '{}' not found", dependencies);
  271. this.dependencies = dependencies;
  272. }
  273. public List<String> getDependencies() {
  274. return dependencies;
  275. }
  276. }
  277. /**
  278. * Indicates that some dependencies have wrong version.
  279. */
  280. public static class DependenciesWrongVersionException extends PluginRuntimeException {
  281. private List<WrongDependencyVersion> dependencies;
  282. public DependenciesWrongVersionException(List<WrongDependencyVersion> dependencies) {
  283. super("Dependencies '{}' have wrong version", dependencies);
  284. this.dependencies = dependencies;
  285. }
  286. public List<WrongDependencyVersion> getDependencies() {
  287. return dependencies;
  288. }
  289. }
  290. }