]> source.dussan.org Git - pf4j.git/commitdiff
Enforce dependencies versions (#150)
authorDecebal Suiu <decebal.suiu@gmail.com>
Sat, 24 Jun 2017 11:19:03 +0000 (14:19 +0300)
committerGitHub <noreply@github.com>
Sat, 24 Jun 2017 11:19:03 +0000 (14:19 +0300)
pf4j/src/main/java/ro/fortsoft/pf4j/AbstractPluginManager.java
pf4j/src/main/java/ro/fortsoft/pf4j/DependencyResolver.java
pf4j/src/main/java/ro/fortsoft/pf4j/PluginDependency.java
pf4j/src/main/java/ro/fortsoft/pf4j/PluginDescriptor.java
pf4j/src/main/java/ro/fortsoft/pf4j/PluginException.java
pf4j/src/main/java/ro/fortsoft/pf4j/PluginNotFoundException.java [deleted file]
pf4j/src/main/java/ro/fortsoft/pf4j/util/DirectedGraph.java
pf4j/src/main/java/ro/fortsoft/pf4j/util/FileUtils.java
pf4j/src/test/java/ro/fortsoft/pf4j/DependencyResolverTest.java [new file with mode: 0644]

index 4bd25fc31a72694295cd4b5e4211b83739b3ee88..71bcb2ed55c61c110c316f054ba4a4677cd108b1 100644 (file)
@@ -50,7 +50,7 @@ public abstract class AbstractPluginManager implements PluginManager {
     protected Map<String, PluginWrapper> plugins;
 
     /*
-     * A map of plugin class loaders (he key is the 'pluginId').
+     * A map of plugin class loaders (the key is the 'pluginId').
      */
     private Map<String, ClassLoader> pluginClassLoaders;
 
@@ -60,7 +60,7 @@ public abstract class AbstractPluginManager implements PluginManager {
     private List<PluginWrapper> unresolvedPlugins;
 
     /**
-     * A list with resolved plugins (resolved dependency).
+     * A list with all resolved plugins (resolved dependency).
      */
     private List<PluginWrapper> resolvedPlugins;
 
@@ -83,7 +83,7 @@ public abstract class AbstractPluginManager implements PluginManager {
     /*
      * The system version used for comparisons to the plugin requires attribute.
      */
-    private Version systemVersion = Version.forIntegers(0, 0, 0);
+    private Version systemVersion = Version.forIntegers(0);
 
     private PluginRepository pluginRepository;
     private PluginFactory pluginFactory;
@@ -394,7 +394,7 @@ public abstract class AbstractPluginManager implements PluginManager {
     }
 
     /**
-     * Stop the specified plugin and it's dependencies.
+     * Stop the specified plugin and it's dependents.
      */
     @Override
     public PluginState stopPlugin(String pluginId) {
@@ -612,7 +612,7 @@ public abstract class AbstractPluginManager implements PluginManager {
             }
         }
 
-        return (version != null) ? Version.valueOf(version) : Version.forIntegers(0, 0, 0);
+        return (version != null) ? Version.valueOf(version) : Version.forIntegers(0);
     }
 
     protected abstract PluginRepository createPluginRepository();
@@ -700,7 +700,7 @@ public abstract class AbstractPluginManager implements PluginManager {
             // If exact versions are not allowed in requires, rewrite to >= expression
             requires = ">=" + requires;
         }
-        if (systemVersion.equals(Version.forIntegers(0,0,0)) || systemVersion.satisfies(requires)) {
+        if (systemVersion.equals(Version.forIntegers(0)) || systemVersion.satisfies(requires)) {
             return true;
         }
 
@@ -718,15 +718,36 @@ public abstract class AbstractPluginManager implements PluginManager {
     }
 
     protected void resolvePlugins() throws PluginException {
-        resolveDependencies();
-    }
+        // extract plugins descriptors from "unresolvedPlugins" list
+        List<PluginDescriptor> descriptors = new ArrayList<>();
+        for (PluginWrapper plugin : unresolvedPlugins) {
+            descriptors.add(plugin.getDescriptor());
+        }
 
-    protected void resolveDependencies() throws PluginException {
-        dependencyResolver.resolve(unresolvedPlugins);
-        resolvedPlugins = dependencyResolver.getSortedPlugins();
-        for (PluginWrapper pluginWrapper : resolvedPlugins) {
+        DependencyResolver.Result result = dependencyResolver.resolve(descriptors);
+
+        if (result.hasCyclicDependency()) {
+            throw new DependencyResolver.CyclicDependencyException();
+        }
+
+        List<String> notFoundDependencies = result.getNotFoundDependencies();
+        if (!notFoundDependencies.isEmpty()) {
+            throw new DependencyResolver.DependenciesNotFoundException(notFoundDependencies);
+        }
+
+        List<DependencyResolver.WrongDependencyVersion> wrongVersionDependencies = result.getWrongVersionDependencies();
+        if (!wrongVersionDependencies.isEmpty()) {
+            throw new DependencyResolver.DependenciesWrongVersionException(wrongVersionDependencies);
+        }
+
+        List<String> sortedPlugins = result.getSortedPlugins();
+
+        // move plugins from "unresolved" to "resolved"
+        for (String pluginId : sortedPlugins) {
+            PluginWrapper pluginWrapper = plugins.get(pluginId);
             unresolvedPlugins.remove(pluginWrapper);
-            log.info("Plugin '{}' resolved", pluginWrapper.getDescriptor().getPluginId());
+            resolvedPlugins.add(pluginWrapper);
+            log.info("Plugin '{}' resolved", pluginId);
         }
     }
 
@@ -800,6 +821,7 @@ public abstract class AbstractPluginManager implements PluginManager {
                 return plugin.getPluginId();
             }
         }
+
         return null;
     }
 
@@ -841,4 +863,5 @@ public abstract class AbstractPluginManager implements PluginManager {
     public void setExactVersionAllowed(boolean exactVersionAllowed) {
         this.exactVersionAllowed = exactVersionAllowed;
     }
+
 }
index d45f677a76e533e439231076dce6fb32a71ac9e1..65c2f332953ab6235e0100aaa8edf7d4c2c2ba43 100644 (file)
  */
 package ro.fortsoft.pf4j;
 
+import com.github.zafarkhaja.semver.Version;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import ro.fortsoft.pf4j.util.DirectedGraph;
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * @author Decebal Suiu
@@ -30,89 +33,263 @@ public class DependencyResolver {
 
        private static final Logger log = LoggerFactory.getLogger(DependencyResolver.class);
 
-    private List<PluginWrapper> plugins;
-    private DirectedGraph<String> dependenciesGraph;
-    private DirectedGraph<String> dependentsGraph;
+    private DirectedGraph<String> dependenciesGraph; // the value is 'pluginId'
+    private DirectedGraph<String> dependentsGraph; // the value is 'pluginId'
     private boolean resolved;
 
-       public void resolve(List<PluginWrapper> plugins) {
-               this.plugins = plugins;
+    public Result resolve(List<PluginDescriptor> plugins) {
+        // create graphs
+        dependenciesGraph = new DirectedGraph<>();
+        dependentsGraph = new DirectedGraph<>();
+
+        // populate graphs
+        Map<String, PluginDescriptor> pluginByIds = new HashMap<>();
+        for (PluginDescriptor plugin : plugins) {
+            addPlugin(plugin);
+            pluginByIds.put(plugin.getPluginId(), plugin);
+        }
+
+        log.debug("Graph: {}", dependenciesGraph);
 
-        initGraph();
+        // get a sorted list of dependencies
+        List<String> sortedPlugins = dependenciesGraph.reverseTopologicalSort();
+        log.debug("Plugins order: {}", sortedPlugins);
+
+        // create the result object
+        Result result = new Result(sortedPlugins);
 
         resolved = true;
-       }
 
-    public List<String> getDependecies(String pluginsId) {
-        if (!resolved) {
-            return Collections.emptyList();
+        if (sortedPlugins != null) { // no cyclic dependency
+            // detect not found dependencies
+            for (String pluginId : sortedPlugins) {
+                if (!pluginByIds.containsKey(pluginId)) {
+                    result.addNotFoundDependency(pluginId);
+                }
+            }
         }
 
-        return dependenciesGraph.getNeighbors(pluginsId);
-    }
+        // check dependencies versions
+        for (PluginDescriptor plugin : plugins) {
+            String pluginId = plugin.getPluginId();
+            Version existingVersion = plugin.getVersion();
 
-    public List<String> getDependents(String pluginsId) {
-        if (!resolved) {
-            return Collections.emptyList();
+            List<String> 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 dependentsGraph.getNeighbors(pluginsId);
+        return result;
     }
 
-       /**
-        * Get the list of plugins in dependency sorted order.
-        */
-       public List<PluginWrapper> getSortedPlugins() throws PluginException {
-        if (!resolved) {
-            return Collections.emptyList();
+    /**
+     * Retrieves the plugins ids that the given plugin id directly depends on.
+     *
+     * @param pluginId
+     * @return
+     */
+    public List<String> getDependencies(String pluginId) {
+        checkResolved();
+        return dependenciesGraph.getNeighbors(pluginId);
+    }
+
+    /**
+     * Retrieves the plugins ids that the given content is a direct dependency of.
+     *
+     * @param pluginId
+     * @return
+     */
+    public List<String> getDependents(String pluginId) {
+        checkResolved();
+        return dependentsGraph.getNeighbors(pluginId);
+    }
+
+    /**
+     * Check if an existing version of dependency is compatible with the required version (from plugin descriptor).
+     *
+     * @param requiredVersion
+     * @param existingVersion
+     * @return
+     */
+    protected boolean checkDependencyVersion(String requiredVersion, Version existingVersion) {
+        return existingVersion.satisfies(requiredVersion);
+    }
+
+    private void addPlugin(PluginDescriptor descriptor) {
+        String pluginId = descriptor.getPluginId();
+        List<PluginDependency> dependencies = descriptor.getDependencies();
+        if (dependencies.isEmpty()) {
+            dependenciesGraph.addVertex(pluginId);
+            dependentsGraph.addVertex(pluginId);
+        } else {
+            for (PluginDependency dependency : dependencies) {
+                dependenciesGraph.addEdge(pluginId, dependency.getPluginId());
+                dependentsGraph.addEdge(dependency.getPluginId(), pluginId);
+            }
         }
+    }
 
-               log.debug("Graph: {}", dependenciesGraph);
-               List<String> pluginsId = dependenciesGraph.reverseTopologicalSort();
+    private void checkResolved() {
+        if (!resolved) {
+            throw new IllegalStateException("Call 'resolve' method first");
+        }
+    }
 
-               if (pluginsId == null) {
-                       throw new PluginException("Cyclic dependencies !!! {}", dependenciesGraph.toString());
-               }
+    private String getDependencyVersionSupport(PluginDescriptor dependent, String dependencyId) {
+        List<PluginDependency> dependencies = dependent.getDependencies();
+        for (PluginDependency dependency : dependencies) {
+            if (dependencyId.equals(dependency.getPluginId())) {
+                return dependency.getPluginVersionSupport();
+            }
+        }
 
-               log.debug("Plugins order: {}", pluginsId);
-               List<PluginWrapper> sortedPlugins = new ArrayList<>();
-               for (String pluginId : pluginsId) {
-                       sortedPlugins.add(getPlugin(pluginId));
-               }
+        throw new IllegalStateException("Cannot find a dependency with id '" + dependencyId +
+            "' for plugin '" + dependent.getPluginId() + "'");
+    }
 
-               return sortedPlugins;
-       }
+       public static class Result {
 
-    private void initGraph() {
-        // create graph
-        dependenciesGraph = new DirectedGraph<>();
-        dependentsGraph = new DirectedGraph<>();
+           private boolean cyclicDependency;
+           private List<String> notFoundDependencies; // value is "pluginId"
+        private List<String> sortedPlugins; // value is "pluginId"
+        private List<WrongDependencyVersion> wrongVersionDependencies;
 
-        // populate graph
-        for (PluginWrapper pluginWrapper : plugins) {
-            PluginDescriptor descriptor = pluginWrapper.getDescriptor();
-            String pluginId = descriptor.getPluginId();
-            List<PluginDependency> dependencies = descriptor.getDependencies();
-            if (!dependencies.isEmpty()) {
-                for (PluginDependency dependency : dependencies) {
-                    dependenciesGraph.addEdge(pluginId, dependency.getPluginId());
-                    dependentsGraph.addEdge(dependency.getPluginId(), pluginId);
-                }
+        Result(List<String> sortedPlugins) {
+            if (sortedPlugins == null) {
+                cyclicDependency = true;
+                this.sortedPlugins = Collections.emptyList();
             } else {
-                dependenciesGraph.addVertex(pluginId);
-                dependentsGraph.addVertex(pluginId);
+                this.sortedPlugins = new ArrayList<>(sortedPlugins);
             }
+
+            notFoundDependencies = new ArrayList<>();
+            wrongVersionDependencies = new ArrayList<>();
+        }
+
+        /**
+         * Returns true is a cyclic dependency was detected.
+         */
+        public boolean hasCyclicDependency() {
+            return cyclicDependency;
+        }
+
+           /**
+            * Returns a list with dependencies required that were not found.
+            */
+        public List<String> getNotFoundDependencies() {
+            return notFoundDependencies;
+        }
+
+        /**
+         * Returns a list with dependencies with wrong version.
+         */
+        public List<WrongDependencyVersion> getWrongVersionDependencies() {
+            return wrongVersionDependencies;
+        }
+
+        /**
+         * Get the list of plugins in dependency sorted order.
+         */
+        public List<String> getSortedPlugins() {
+            return sortedPlugins;
         }
+
+        void addNotFoundDependency(String pluginId) {
+            notFoundDependencies.add(pluginId);
+        }
+
+        void addWrongDependencyVersion(WrongDependencyVersion wrongDependencyVersion) {
+            wrongVersionDependencies.add(wrongDependencyVersion);
+        }
+
+    }
+
+    public static class WrongDependencyVersion {
+
+        private String dependencyId; // value is "pluginId"
+        private String dependentId; // value is "pluginId"
+        private Version existingVersion;
+        private String requiredVersion;
+
+        WrongDependencyVersion(String dependencyId, String dependentId, Version 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 Version getExistingVersion() {
+            return existingVersion;
+        }
+
+        public String getRequiredVersion() {
+            return requiredVersion;
+        }
+
+    }
+
+    /**
+     * It will be thrown if a cyclic dependency is detected.
+     */
+    public static class CyclicDependencyException extends PluginException {
+
+        public CyclicDependencyException() {
+            super("Cyclic dependencies");
+        }
+
+    }
+
+    /**
+     * Indicates that the dependencies required were not found.
+     */
+    public static class DependenciesNotFoundException extends PluginException {
+
+        private List<String> dependencies;
+
+        public DependenciesNotFoundException(List<String> dependencies) {
+            super("Dependencies '{}' not found", dependencies);
+
+            this.dependencies = dependencies;
+        }
+
+        public List<String> getDependencies() {
+            return dependencies;
+        }
+
     }
 
-    private PluginWrapper getPlugin(String pluginId) throws PluginNotFoundException {
-               for (PluginWrapper pluginWrapper : plugins) {
-                       if (pluginId.equals(pluginWrapper.getDescriptor().getPluginId())) {
-                               return pluginWrapper;
-                       }
-               }
+    /**
+     * Indicates that some dependencies have wrong version.
+     */
+    public static class DependenciesWrongVersionException extends PluginException {
+
+        private List<WrongDependencyVersion> dependencies;
+
+        public DependenciesWrongVersionException(List<WrongDependencyVersion> dependencies) {
+            super("Dependencies '{}' have wrong version", dependencies);
 
-               throw new PluginNotFoundException(pluginId);
-       }
+            this.dependencies = dependencies;
+        }
+
+        public List<WrongDependencyVersion> getDependencies() {
+            return dependencies;
+        }
+
+    }
 
 }
index 0e5930ac861c424ddc6fddbf308195da643ac642..44a147ff32e18e78a6f257c3f9a484a9303bf6e1 100644 (file)
@@ -28,7 +28,6 @@ public class PluginDependency {
         if (index == -1) {
             this.pluginId = dependency;
         } else {
-
             this.pluginId = dependency.substring(0, index);
             if (dependency.length() > index + 1) {
                 this.pluginVersionSupport = dependency.substring(index + 1);
index 65a1d43baa97cbf09451962e286b8c7c9cd1ea7f..c6b7db891ce2a9e2502e5cac188cca3e585017f2 100644 (file)
@@ -109,31 +109,43 @@ public class PluginDescriptor {
                                + license + "]";
        }
 
-       void setPluginId(String pluginId) {
+       PluginDescriptor setPluginId(String pluginId) {
         this.pluginId = pluginId;
+
+        return this;
     }
 
-       void setPluginDescription(String pluginDescription) {
+    PluginDescriptor setPluginDescription(String pluginDescription) {
         this.pluginDescription = pluginDescription;
+
+        return this;
     }
 
-    void setPluginClass(String pluginClassName) {
+    PluginDescriptor setPluginClass(String pluginClassName) {
         this.pluginClass = pluginClassName;
+
+        return this;
     }
 
-    void setPluginVersion(Version version) {
+    PluginDescriptor setPluginVersion(Version version) {
         this.version = version;
+
+        return this;
     }
 
-    void setProvider(String provider) {
+    PluginDescriptor setProvider(String provider) {
         this.provider = provider;
+
+        return this;
     }
 
-    void setRequires(String requires) {
+    PluginDescriptor setRequires(String requires) {
         this.requires = requires;
+
+        return this;
     }
 
-    void setDependencies(String dependencies) {
+    PluginDescriptor setDependencies(String dependencies) {
        if (dependencies != null) {
                dependencies = dependencies.trim();
                if (dependencies.isEmpty()) {
@@ -154,10 +166,14 @@ public class PluginDescriptor {
        } else {
                this.dependencies = Collections.emptyList();
        }
+
+       return this;
     }
 
-    public void setLicense(String license) {
+    public PluginDescriptor setLicense(String license) {
         this.license = license;
+
+        return this;
     }
 
 }
index caa194a74f399210d37f3e65d811d500bf928477..3d4aec4967093e69480bd0eeee2667bd676b63c5 100644 (file)
@@ -24,8 +24,6 @@ import ro.fortsoft.pf4j.util.StringUtils;
  */
 public class PluginException extends Exception {
 
-       private static final long serialVersionUID = 1L;
-
        public PluginException() {
         super();
     }
diff --git a/pf4j/src/main/java/ro/fortsoft/pf4j/PluginNotFoundException.java b/pf4j/src/main/java/ro/fortsoft/pf4j/PluginNotFoundException.java
deleted file mode 100644 (file)
index 7ea4b53..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright 2012 Decebal Suiu
- *
- * 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 ro.fortsoft.pf4j;
-
-/**
- * @author Decebal Suiu
- */
-class PluginNotFoundException extends PluginException {
-
-       private static final long serialVersionUID = 1L;
-
-       private String pluginId;
-
-       public PluginNotFoundException(String pluginId) {
-               super("Plugin '" + pluginId + "' not found.");
-
-               this.pluginId = pluginId;
-       }
-
-       public String getPluginId() {
-               return pluginId;
-       }
-
-}
index 9a37f08001bdc2e4c554ad7765e55b3301cdedd3..8970c783e0336aa541f148a7a5f4c65b43c0e1fe 100644 (file)
@@ -23,6 +23,8 @@ import java.util.Map;
 import java.util.Stack;
 
 /**
+ * See <a href="https://en.wikipedia.org/wiki/Directed_graph">Wikipedia</a> for more information.
+ *
  * @author Decebal Suiu
  */
 public class DirectedGraph<V> {
@@ -38,7 +40,7 @@ public class DirectedGraph<V> {
      * Add a vertex to the graph. Nothing happens if vertex is already in graph.
      */
     public void addVertex(V vertex) {
-        if (neighbors.containsKey(vertex)) {
+        if (containsVertex(vertex)) {
                return;
         }
 
@@ -52,38 +54,42 @@ public class DirectedGraph<V> {
         return neighbors.containsKey(vertex);
     }
 
+    public void removeVertex(V vertex) {
+        neighbors.remove(vertex);
+    }
+
     /**
      * Add an edge to the graph; if either vertex does not exist, it's added.
      * This implementation allows the creation of multi-edges and self-loops.
      */
     public void addEdge(V from, V to) {
-        this.addVertex(from);
-        this.addVertex(to);
+        addVertex(from);
+        addVertex(to);
         neighbors.get(from).add(to);
     }
 
     /**
      * Remove an edge from the graph. Nothing happens if no such edge.
-     * @throws IllegalArgumentException if either vertex doesn't exist.
+     * @throws {@link IllegalArgumentException} if either vertex doesn't exist.
      */
-    public void remove(V from, V to) {
-        if (!(this.containsVertex(from) && this.containsVertex(to))) {
-            throw new IllegalArgumentException("Nonexistent vertex");
+    public void removeEdge(V from, V to) {
+        if (!containsVertex(from)) {
+            throw new IllegalArgumentException("Nonexistent vertex " + from);
+        }
+
+        if (!containsVertex(to)) {
+            throw new IllegalArgumentException("Nonexistent vertex " + to);
         }
 
         neighbors.get(from).remove(to);
     }
 
     public List<V> getNeighbors(V vertex) {
-        if (!neighbors.containsKey(vertex)) {
-               return new ArrayList<V>();
-        }
-
-        return neighbors.get(vertex);
+        return containsVertex(vertex) ? neighbors.get(vertex) : new ArrayList<V>();
     }
 
     /**
-     * Report (as a Map) the out-degree of each vertex.
+     * Report (as a Map) the out-degree (the number of tail ends adjacent to a vertex) of each vertex.
      */
     public Map<V, Integer> outDegree() {
         Map<V, Integer> result = new HashMap<>();
@@ -95,9 +101,9 @@ public class DirectedGraph<V> {
     }
 
     /**
-     * Report (as a Map) the in-degree of each vertex.
+     * Report (as a Map) the in-degree (the number of head ends adjacent to a vertex) of each vertex.
      */
-    public Map<V,Integer> inDegree() {
+    public Map<V, Integer> inDegree() {
         Map<V, Integer> result = new HashMap<>();
         for (V vertex : neighbors.keySet()) {
                result.put(vertex, 0); // all in-degrees are 0
@@ -113,6 +119,7 @@ public class DirectedGraph<V> {
 
     /**
      * Report (as a List) the topological sort of the vertices; null for no such sort.
+     * See <a href="https://en.wikipedia.org/wiki/Topological_sorting">this</a> for more information.
      */
     public List<V> topologicalSort() {
         Map<V, Integer> degree = inDegree();
@@ -156,6 +163,7 @@ public class DirectedGraph<V> {
        if (list == null) {
                return null;
        }
+
        Collections.reverse(list);
 
        return list;
index 03d86bf08d94685cab81b1a17a67a4bf3d8318c0..550243a0a8ca12fc63daaab9e8a344d4333f2382 100644 (file)
@@ -18,7 +18,13 @@ package ro.fortsoft.pf4j.util;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.*;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileFilter;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
 import java.nio.file.FileVisitResult;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -26,7 +32,6 @@ import java.nio.file.SimpleFileVisitor;
 import java.nio.file.attribute.BasicFileAttributes;
 import java.nio.file.attribute.FileTime;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
 
@@ -34,9 +39,8 @@ import java.util.List;
  * @author Decebal Suiu
  */
 public class FileUtils {
-    private static final Logger log = LoggerFactory.getLogger(FileUtils.class);
 
-    private static final List<String> ZIP_EXTENSIONS = Arrays.asList(".zip", ".ZIP", ".Zip");
+    private static final Logger log = LoggerFactory.getLogger(FileUtils.class);
 
     public static List<String> readLines(File file, boolean ignoreComments) throws IOException {
                if (!file.exists() || !file.isFile()) {
@@ -86,19 +90,23 @@ public class FileUtils {
         */
     public static void delete(Path path) throws IOException {
         Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
+
            @Override
            public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException {
                if (!attrs.isSymbolicLink()) {
                    Files.delete(path);
                }
+
                return FileVisitResult.CONTINUE;
            }
 
            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                Files.delete(dir);
+
                return FileVisitResult.CONTINUE;
            }
+
         });
        }
 
@@ -140,6 +148,7 @@ public class FileUtils {
                 return newPath;
             }
         }
+
         return null;
     }
 
@@ -151,6 +160,7 @@ public class FileUtils {
         if (path == null) {
             return;
         }
+
         try {
             Files.delete(path);
         } catch (IOException ignored) { }
@@ -202,4 +212,5 @@ public class FileUtils {
     public static boolean isZipFile(Path path) {
         return Files.isRegularFile(path) && path.toString().toLowerCase().endsWith(".zip");
     }
+
 }
diff --git a/pf4j/src/test/java/ro/fortsoft/pf4j/DependencyResolverTest.java b/pf4j/src/test/java/ro/fortsoft/pf4j/DependencyResolverTest.java
new file mode 100644 (file)
index 0000000..e8d1749
--- /dev/null
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2017 Decebal Suiu
+ *
+ * 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 ro.fortsoft.pf4j;
+
+import com.github.zafarkhaja.semver.Version;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author Decebal Suiu
+ */
+public class DependencyResolverTest {
+
+    @Test
+    public void sortedPlugins() {
+        // create incomplete plugin descriptor (ignore some attributes)
+        PluginDescriptor pd1 = new PluginDescriptor()
+            .setPluginId("p1")
+            .setDependencies("p2");
+
+        PluginDescriptor pd2 = new PluginDescriptor()
+            .setPluginId("p2")
+            .setPluginVersion(Version.forIntegers(0)); // needed in "checkDependencyVersion" method
+
+        List<PluginDescriptor> plugins = new ArrayList<>();
+        plugins.add(pd1);
+        plugins.add(pd2);
+
+        DependencyResolver resolver = new DependencyResolver();
+        DependencyResolver.Result result = resolver.resolve(plugins);
+
+        assertTrue(result.getNotFoundDependencies().isEmpty());
+        assertEquals(result.getSortedPlugins(), Arrays.asList("p2", "p1"));
+    }
+
+    @Test
+    public void notFoundDependencies() throws Exception {
+        PluginDescriptor pd1 = new PluginDescriptor()
+            .setPluginId("p1")
+            .setDependencies("p2, p3");
+
+        List<PluginDescriptor> plugins = new ArrayList<>();
+        plugins.add(pd1);
+
+        DependencyResolver resolver = new DependencyResolver();
+        DependencyResolver.Result result = resolver.resolve(plugins);
+
+        assertFalse(result.getNotFoundDependencies().isEmpty());
+        assertEquals(result.getNotFoundDependencies(), Arrays.asList("p2", "p3"));
+    }
+
+    @Test
+    public void cyclicDependencies() {
+        PluginDescriptor pd1 = new PluginDescriptor()
+            .setPluginId("p1")
+            .setPluginVersion(Version.forIntegers(0))
+            .setDependencies("p2");
+
+        PluginDescriptor pd2 = new PluginDescriptor()
+            .setPluginId("p2")
+            .setPluginVersion(Version.forIntegers(0))
+            .setDependencies("p3");
+
+        PluginDescriptor pd3 = new PluginDescriptor()
+            .setPluginId("p3")
+            .setPluginVersion(Version.forIntegers(0))
+            .setDependencies("p1");
+
+        List<PluginDescriptor> plugins = new ArrayList<>();
+        plugins.add(pd1);
+        plugins.add(pd2);
+        plugins.add(pd3);
+
+        DependencyResolver resolver = new DependencyResolver();
+        DependencyResolver.Result result = resolver.resolve(plugins);
+
+        assertTrue(result.hasCyclicDependency());
+    }
+
+    @Test
+    public void wrongDependencyVersion() {
+        PluginDescriptor pd1 = new PluginDescriptor()
+            .setPluginId("p1")
+//            .setDependencies("p2@2.0.0"); // simple version
+            .setDependencies("p2@>=1.5.0 & <1.6.0"); // range version
+
+        PluginDescriptor pd2 = new PluginDescriptor()
+            .setPluginId("p2")
+            .setPluginVersion(Version.forIntegers(1, 4));
+
+        List<PluginDescriptor> plugins = new ArrayList<>();
+        plugins.add(pd1);
+        plugins.add(pd2);
+
+        DependencyResolver resolver = new DependencyResolver();
+        DependencyResolver.Result result = resolver.resolve(plugins);
+
+        assertFalse(result.getWrongVersionDependencies().isEmpty());
+    }
+
+    @Test
+    public void goodDependencyVersion() {
+        PluginDescriptor pd1 = new PluginDescriptor()
+            .setPluginId("p1")
+            .setDependencies("p2@2.0.0");
+
+        PluginDescriptor pd2 = new PluginDescriptor()
+            .setPluginId("p2")
+            .setPluginVersion(Version.forIntegers(2));
+
+        List<PluginDescriptor> plugins = new ArrayList<>();
+        plugins.add(pd1);
+        plugins.add(pd2);
+
+        DependencyResolver resolver = new DependencyResolver();
+        DependencyResolver.Result result = resolver.resolve(plugins);
+
+        assertTrue(result.getWrongVersionDependencies().isEmpty());
+    }
+
+}