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 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  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. *
  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 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. public Result resolve(List<PluginDescriptor> plugins) {
  47. // create graphs
  48. dependenciesGraph = new DirectedGraph<>();
  49. dependentsGraph = new DirectedGraph<>();
  50. // populate graphs
  51. Map<String, PluginDescriptor> pluginByIds = new HashMap<>();
  52. for (PluginDescriptor plugin : plugins) {
  53. addPlugin(plugin);
  54. pluginByIds.put(plugin.getPluginId(), plugin);
  55. }
  56. log.debug("Graph: {}", dependenciesGraph);
  57. // get a sorted list of dependencies
  58. List<String> sortedPlugins = dependenciesGraph.reverseTopologicalSort();
  59. log.debug("Plugins order: {}", sortedPlugins);
  60. // create the result object
  61. Result result = new Result(sortedPlugins);
  62. resolved = true;
  63. if (sortedPlugins != null) { // no cyclic dependency
  64. // detect not found dependencies
  65. for (String pluginId : sortedPlugins) {
  66. if (!pluginByIds.containsKey(pluginId)) {
  67. result.addNotFoundDependency(pluginId);
  68. }
  69. }
  70. }
  71. // check dependencies versions
  72. for (PluginDescriptor plugin : plugins) {
  73. String pluginId = plugin.getPluginId();
  74. String existingVersion = plugin.getVersion();
  75. List<String> dependents = getDependents(pluginId);
  76. while (!dependents.isEmpty()) {
  77. String dependentId = dependents.remove(0);
  78. PluginDescriptor dependent = pluginByIds.get(dependentId);
  79. String requiredVersion = getDependencyVersionSupport(dependent, pluginId);
  80. boolean ok = checkDependencyVersion(requiredVersion, existingVersion);
  81. if (!ok) {
  82. result.addWrongDependencyVersion(new WrongDependencyVersion(pluginId, dependentId, existingVersion, requiredVersion));
  83. }
  84. }
  85. }
  86. return result;
  87. }
  88. /**
  89. * Retrieves the plugins ids that the given plugin id directly depends on.
  90. *
  91. * @param pluginId the unique plugin identifier, specified in its metadata
  92. * @return an immutable list of dependencies (new list for each call)
  93. */
  94. public List<String> getDependencies(String pluginId) {
  95. checkResolved();
  96. return new ArrayList<>(dependenciesGraph.getNeighbors(pluginId));
  97. }
  98. /**
  99. * Retrieves the plugins ids that the given content is a direct dependency of.
  100. *
  101. * @param pluginId the unique plugin identifier, specified in its metadata
  102. * @return an immutable list of dependents (new list for each call)
  103. */
  104. public List<String> getDependents(String pluginId) {
  105. checkResolved();
  106. return new ArrayList<>(dependentsGraph.getNeighbors(pluginId));
  107. }
  108. /**
  109. * Check if an existing version of dependency is compatible with the required version (from plugin descriptor).
  110. *
  111. * @param requiredVersion
  112. * @param existingVersion
  113. * @return
  114. */
  115. protected boolean checkDependencyVersion(String requiredVersion, String existingVersion) {
  116. return versionManager.checkVersionConstraint(existingVersion, requiredVersion);
  117. }
  118. private void addPlugin(PluginDescriptor descriptor) {
  119. String pluginId = descriptor.getPluginId();
  120. List<PluginDependency> dependencies = descriptor.getDependencies();
  121. if (dependencies.isEmpty()) {
  122. dependenciesGraph.addVertex(pluginId);
  123. dependentsGraph.addVertex(pluginId);
  124. } else {
  125. boolean edgeAdded = false;
  126. for (PluginDependency dependency : dependencies) {
  127. // Don't register optional plugins in the dependency graph to avoid automatic disabling of the plugin,
  128. // if an optional dependency is missing.
  129. if (!dependency.isOptional()) {
  130. edgeAdded = true;
  131. dependenciesGraph.addEdge(pluginId, dependency.getPluginId());
  132. dependentsGraph.addEdge(dependency.getPluginId(), pluginId);
  133. }
  134. }
  135. // Register the plugin without dependencies, if all of its dependencies are optional.
  136. if (!edgeAdded) {
  137. dependenciesGraph.addVertex(pluginId);
  138. dependentsGraph.addVertex(pluginId);
  139. }
  140. }
  141. }
  142. private void checkResolved() {
  143. if (!resolved) {
  144. throw new IllegalStateException("Call 'resolve' method first");
  145. }
  146. }
  147. private String getDependencyVersionSupport(PluginDescriptor dependent, String dependencyId) {
  148. List<PluginDependency> dependencies = dependent.getDependencies();
  149. for (PluginDependency dependency : dependencies) {
  150. if (dependencyId.equals(dependency.getPluginId())) {
  151. return dependency.getPluginVersionSupport();
  152. }
  153. }
  154. throw new IllegalStateException("Cannot find a dependency with id '" + dependencyId +
  155. "' for plugin '" + dependent.getPluginId() + "'");
  156. }
  157. public static class Result {
  158. private boolean cyclicDependency;
  159. private List<String> notFoundDependencies; // value is "pluginId"
  160. private List<String> sortedPlugins; // value is "pluginId"
  161. private List<WrongDependencyVersion> wrongVersionDependencies;
  162. Result(List<String> sortedPlugins) {
  163. if (sortedPlugins == null) {
  164. cyclicDependency = true;
  165. this.sortedPlugins = Collections.emptyList();
  166. } else {
  167. this.sortedPlugins = new ArrayList<>(sortedPlugins);
  168. }
  169. notFoundDependencies = new ArrayList<>();
  170. wrongVersionDependencies = new ArrayList<>();
  171. }
  172. /**
  173. * Returns true is a cyclic dependency was detected.
  174. */
  175. public boolean hasCyclicDependency() {
  176. return cyclicDependency;
  177. }
  178. /**
  179. * Returns a list with dependencies required that were not found.
  180. */
  181. public List<String> getNotFoundDependencies() {
  182. return notFoundDependencies;
  183. }
  184. /**
  185. * Returns a list with dependencies with wrong version.
  186. */
  187. public List<WrongDependencyVersion> getWrongVersionDependencies() {
  188. return wrongVersionDependencies;
  189. }
  190. /**
  191. * Get the list of plugins in dependency sorted order.
  192. */
  193. public List<String> getSortedPlugins() {
  194. return sortedPlugins;
  195. }
  196. void addNotFoundDependency(String pluginId) {
  197. notFoundDependencies.add(pluginId);
  198. }
  199. void addWrongDependencyVersion(WrongDependencyVersion wrongDependencyVersion) {
  200. wrongVersionDependencies.add(wrongDependencyVersion);
  201. }
  202. }
  203. public static class WrongDependencyVersion {
  204. private String dependencyId; // value is "pluginId"
  205. private String dependentId; // value is "pluginId"
  206. private String existingVersion;
  207. private String requiredVersion;
  208. WrongDependencyVersion(String dependencyId, String dependentId, String existingVersion, String requiredVersion) {
  209. this.dependencyId = dependencyId;
  210. this.dependentId = dependentId;
  211. this.existingVersion = existingVersion;
  212. this.requiredVersion = requiredVersion;
  213. }
  214. public String getDependencyId() {
  215. return dependencyId;
  216. }
  217. public String getDependentId() {
  218. return dependentId;
  219. }
  220. public String getExistingVersion() {
  221. return existingVersion;
  222. }
  223. public String getRequiredVersion() {
  224. return requiredVersion;
  225. }
  226. @Override
  227. public String toString() {
  228. return "WrongDependencyVersion{" +
  229. "dependencyId='" + dependencyId + '\'' +
  230. ", dependentId='" + dependentId + '\'' +
  231. ", existingVersion='" + existingVersion + '\'' +
  232. ", requiredVersion='" + requiredVersion + '\'' +
  233. '}';
  234. }
  235. }
  236. /**
  237. * It will be thrown if a cyclic dependency is detected.
  238. */
  239. public static class CyclicDependencyException extends PluginRuntimeException {
  240. public CyclicDependencyException() {
  241. super("Cyclic dependencies");
  242. }
  243. }
  244. /**
  245. * Indicates that the dependencies required were not found.
  246. */
  247. public static class DependenciesNotFoundException extends PluginRuntimeException {
  248. private List<String> dependencies;
  249. public DependenciesNotFoundException(List<String> dependencies) {
  250. super("Dependencies '{}' not found", dependencies);
  251. this.dependencies = dependencies;
  252. }
  253. public List<String> getDependencies() {
  254. return dependencies;
  255. }
  256. }
  257. /**
  258. * Indicates that some dependencies have wrong version.
  259. */
  260. public static class DependenciesWrongVersionException extends PluginRuntimeException {
  261. private List<WrongDependencyVersion> dependencies;
  262. public DependenciesWrongVersionException(List<WrongDependencyVersion> dependencies) {
  263. super("Dependencies '{}' have wrong version", dependencies);
  264. this.dependencies = dependencies;
  265. }
  266. public List<WrongDependencyVersion> getDependencies() {
  267. return dependencies;
  268. }
  269. }
  270. }