diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginLoader.java b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginLoader.java index 6696056e8d..946f65d81e 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginLoader.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginLoader.java @@ -226,7 +226,7 @@ public class DefaultPluginLoader implements PluginLoader { injectionModules.add((Module) extensionClass.newInstance()); } - catch (Exception ex) + catch (IllegalAccessException | InstantiationException ex) { logger.error("could not create instance of module", ex); } diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginNode.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginNode.java new file mode 100644 index 0000000000..281fb2eab1 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginNode.java @@ -0,0 +1,191 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.plugin; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; + +//~--- JDK imports ------------------------------------------------------------ + +import java.util.List; + +/** + * + * @author Sebastian Sdorra + */ +public final class PluginNode +{ + + /** + * Constructs ... + * + * + * @param plugin + */ + public PluginNode(ExplodedSmp plugin) + { + this.plugin = plugin; + } + + //~--- methods -------------------------------------------------------------- + + /** + * Method description + * + * + * @param node + */ + public void addChild(PluginNode node) + { + this.children.add(node); + node.addParent(this); + } + + /** + * Method description + * + * + * @param node + */ + private void addParent(PluginNode node) + { + this.parents.add(node); + } + + //~--- get methods ---------------------------------------------------------- + + /** + * Method description + * + * + * @param id + * + * @return + */ + public PluginNode getChild(final String id) + { + return Iterables.find(children, new Predicate() + { + + @Override + public boolean apply(PluginNode node) + { + return node.getId().equals(id); + } + }); + } + + /** + * Method description + * + * + * @return + */ + public List getChildren() + { + return children; + } + + /** + * Method description + * + * + * @return + */ + public String getId() + { + return plugin.getPlugin().getInformation().getId(false); + } + + /** + * Method description + * + * + * @return + */ + public List getParents() + { + return parents; + } + + /** + * Method description + * + * + * @return + */ + public ExplodedSmp getPlugin() + { + return plugin; + } + + /** + * Method description + * + * + * @return + */ + public PluginWrapper getWrapper() + { + return wrapper; + } + + //~--- set methods ---------------------------------------------------------- + + /** + * Method description + * + * + * @param wrapper + */ + public void setWrapper(PluginWrapper wrapper) + { + this.wrapper = wrapper; + } + + //~--- fields --------------------------------------------------------------- + + /** Field description */ + private final List parents = Lists.newArrayList(); + + /** Field description */ + private final List children = Lists.newArrayList(); + + /** Field description */ + private final ExplodedSmp plugin; + + /** Field description */ + private PluginWrapper wrapper; +} diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java index 21ade9f749..506c39201e 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java @@ -37,7 +37,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList.Builder; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; -import com.google.common.collect.Ordering; import com.google.common.collect.Sets; import com.google.common.hash.Hashing; @@ -147,7 +146,7 @@ public final class PluginProcessor * * @throws IOException */ - private static DefaultPluginClassLoader createClassLoader( + private DefaultPluginClassLoader createClassLoader( ClassLoader parentClassLoader, Path directory) throws IOException { @@ -256,17 +255,97 @@ public final class PluginProcessor } List smps = Lists.transform(dirs, new PathTransformer()); - Iterable smpOrdered = Ordering.natural().sortedCopy(smps); - Set pluginWrappers = createPluginWrappers(classLoader, - smpOrdered); + logger.trace("start building plugin tree"); + + List rootNodes = new PluginTree(smps).getRootNodes(); + + logger.trace("create plugin wrappers and build classloaders"); + + Set wrappers = createPluginWrappers(classLoader, rootNodes); if (logger.isDebugEnabled()) { - logger.debug("collected {} plugins", pluginWrappers.size()); + logger.debug("collected {} plugins", wrappers.size()); } - return ImmutableSet.copyOf(pluginWrappers); + return ImmutableSet.copyOf(wrappers); + } + + /** + * Method description + * + * + * @param plugins + * @param classLoader + * @param node + * + * @throws IOException + */ + private void appendPluginWrapper(Set plugins, + ClassLoader classLoader, PluginNode node) + throws IOException + { + ExplodedSmp smp = node.getPlugin(); + + List parents = Lists.newArrayList(); + + for (PluginNode parent : node.getParents()) + { + PluginWrapper wrapper = parent.getWrapper(); + + if (wrapper != null) + { + parents.add(wrapper.getClassLoader()); + } + else + { + //J- + throw new PluginLoadException( + String.format( + "parent %s of plugin %s is not ready", parent.getId(), node.getId() + ) + ); + //J+ + } + + } + + PluginWrapper plugin = + createPluginWrapper(createParentPluginClassLoader(classLoader, parents), + smp.getPath()); + + if (plugin != null) + { + plugins.add(plugin); + } + } + + /** + * Method description + * + * + * @param plugins + * @param classLoader + * @param nodes + * + * @throws IOException + */ + private void appendPluginWrappers(Set plugins, + ClassLoader classLoader, List nodes) + throws IOException + { + + // TODO fix plugin loading order + for (PluginNode node : nodes) + { + appendPluginWrapper(plugins, classLoader, node); + } + + for (PluginNode node : nodes) + { + appendPluginWrappers(plugins, classLoader, node.getChildren()); + } } /** @@ -366,6 +445,37 @@ public final class PluginProcessor } } + /** + * Method description + * + * + * @param root + * @param parents + * + * @return + */ + private ClassLoader createParentPluginClassLoader(ClassLoader root, + List parents) + { + ClassLoader result; + int size = parents.size(); + + if (size == 0) + { + result = root; + } + else if (size == 1) + { + result = parents.get(0); + } + else + { + result = new MultiParentClassLoader(parents); + } + + return result; + } + /** * Method description * @@ -407,26 +517,19 @@ public final class PluginProcessor * * @param classLoader * @param smps + * @param rootNodes * * @return * * @throws IOException */ private Set createPluginWrappers(ClassLoader classLoader, - Iterable smps) + List rootNodes) throws IOException { Set plugins = Sets.newHashSet(); - for (ExplodedSmp smp : smps) - { - PluginWrapper plugin = createPluginWrapper(classLoader, smp.getPath()); - - if (plugin != null) - { - plugins.add(plugin); - } - } + appendPluginWrappers(plugins, classLoader, rootNodes); return plugins; } diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginTree.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginTree.java new file mode 100644 index 0000000000..e44076e65f --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginTree.java @@ -0,0 +1,210 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.plugin; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.collect.Lists; +import com.google.common.collect.Ordering; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +//~--- JDK imports ------------------------------------------------------------ + +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +/** + * + * @author Sebastian Sdorra + */ +public final class PluginTree +{ + + /** + * the logger for PluginTree + */ + private static final Logger logger = + LoggerFactory.getLogger(PluginTree.class); + + //~--- constructors --------------------------------------------------------- + + /** + * Constructs ... + * + * + * @param smps + */ + public PluginTree(ExplodedSmp... smps) + { + this(Arrays.asList(smps)); + } + + /** + * Constructs ... + * + * + * @param smps + */ + public PluginTree(List smps) + { + Iterable smpOrdered = Ordering.natural().sortedCopy(smps); + + for (ExplodedSmp smp : smpOrdered) + { + Plugin plugin = smp.getPlugin(); + PluginCondition condition = plugin.getCondition(); + + if ((condition == null) || condition.isSupported()) + { + Set dependencies = plugin.getDependencies(); + + if ((dependencies == null) || dependencies.isEmpty()) + { + rootNodes.add(new PluginNode(smp)); + } + else + { + appendNode(rootNodes, dependencies, smp); + } + } + else + { + //J- + throw new PluginConditionFailedException( + condition, + String.format( + "could not load plugin %s, the plugin condition does not match", + plugin.getInformation().getId() + ) + ); + //J+ + } + } + } + + //~--- get methods ---------------------------------------------------------- + + /** + * Method description + * + * + * @return + */ + public List getRootNodes() + { + return rootNodes; + } + + //~--- methods -------------------------------------------------------------- + + /** + * Method description + * + * + * @param nodes + * @param dependencies + * @param smp + */ + private void appendNode(List nodes, Set dependencies, + ExplodedSmp smp) + { + PluginNode child = new PluginNode(smp); + + for (String dependency : dependencies) + { + if (!appendNode(nodes, child, dependency)) + { + //J- + throw new PluginNotInstalledException( + String.format( + "dependency %s of %s is not installed", + dependency, + child.getId() + ) + ); + //J+ + } + } + } + + /** + * Method description + * + * + * @param nodes + * @param child + * @param dependency + * + * @return + */ + private boolean appendNode(List nodes, PluginNode child, + String dependency) + { + logger.debug("check for {} {}", dependency, child.getId()); + + boolean found = false; + + for (PluginNode node : nodes) + { + if (node.getId().equals(dependency)) + { + logger.debug("add plugin {} as child of {}", child.getId(), + node.getId()); + node.addChild(child); + found = true; + + break; + } + else + { + if (appendNode(node.getChildren(), child, dependency)) + { + found = true; + + break; + } + + } + } + + return found; + } + + + //~--- fields --------------------------------------------------------------- + + /** Field description */ + private final List rootNodes = Lists.newArrayList(); +} diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginProcessorTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginProcessorTest.java new file mode 100644 index 0000000000..02e35da8de --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginProcessorTest.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.plugin; + +import java.io.IOException; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.rules.TemporaryFolder; + +/** + * + * @author Sebastian Sdorra + */ +public class PluginProcessorTest +{ + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Test + public void testBuildPluginTree() throws IOException + { + PluginProcessor processor = new PluginProcessor(temp.newFolder().toPath()); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginTreeTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginTreeTest.java new file mode 100644 index 0000000000..0c32791813 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginTreeTest.java @@ -0,0 +1,268 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.plugin; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.base.Function; +import com.google.common.collect.Lists; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import static org.hamcrest.Matchers.*; + +import static org.junit.Assert.*; + +//~--- JDK imports ------------------------------------------------------------ + +import java.io.IOException; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * + * @author Sebastian Sdorra + */ +public class PluginTreeTest +{ + + /** + * Method description + * + * + * @throws IOException + */ + @Test(expected = PluginConditionFailedException.class) + public void testPluginConditionFailed() throws IOException + { + PluginCondition condition = new PluginCondition("999", + new ArrayList(), "hit"); + Plugin plugin = new Plugin(createInfo("a", "b", "1"), null, condition, + null); + ExplodedSmp smp = createSmp(plugin); + + new PluginTree(smp).getRootNodes(); + } + + /** + * Method description + * + * + * @throws IOException + */ + @Test(expected = PluginNotInstalledException.class) + public void testPluginNotInstalled() throws IOException + { + new PluginTree(createSmpWithDependency("b", "a")).getRootNodes(); + } + + /** + * Method description + * + * + * @throws IOException + */ + @Test + public void testRootNotes() throws IOException + { + List smps = createSmps("a", "b", "c"); + List nodes = unwrapIds(new PluginTree(smps).getRootNodes()); + + assertThat(nodes, containsInAnyOrder("a:a", "b:b", "c:c")); + } + + /** + * Method description + * + * + * @throws IOException + */ + @Test + public void testSimpleDependencies() throws IOException + { + //J- + ExplodedSmp[] smps = new ExplodedSmp[] { + createSmpWithDependency("a"), + createSmpWithDependency("b", "a"), + createSmpWithDependency("c", "a", "b") + }; + //J+ + + PluginTree tree = new PluginTree(smps); + List rootNodes = tree.getRootNodes(); + + assertThat(unwrapIds(rootNodes), containsInAnyOrder("a:a")); + + PluginNode a = rootNodes.get(0); + + assertThat(unwrapIds(a.getChildren()), containsInAnyOrder("b:b", "c:c")); + + PluginNode b = a.getChild("b:b"); + + assertThat(unwrapIds(b.getChildren()), containsInAnyOrder("c:c")); + } + + /** + * Method description + * + * + * @param groupId + * @param artifactId + * @param version + * + * @return + */ + private PluginInformation createInfo(String groupId, String artifactId, + String version) + { + PluginInformation info = new PluginInformation(); + + info.setGroupId(groupId); + info.setArtifactId(artifactId); + info.setVersion(version); + + return info; + } + + /** + * Method description + * + * + * @param plugin + * + * @return + * + * @throws IOException + */ + private ExplodedSmp createSmp(Plugin plugin) throws IOException + { + return new ExplodedSmp(tempFolder.newFile().toPath(), plugin); + } + + /** + * Method description + * + * + * @param name + * + * @return + * + * @throws IOException + */ + private ExplodedSmp createSmp(String name) throws IOException + { + return createSmp(new Plugin(createInfo(name, name, "1.0.0"), null, null, + null)); + } + + /** + * Method description + * + * + * @param name + * @param dependencies + * + * @return + * + * @throws IOException + */ + private ExplodedSmp createSmpWithDependency(String name, + String... dependencies) + throws IOException + { + Set dependencySet = new HashSet<>(); + + for (String d : dependencies) + { + dependencySet.add(d.concat(":").concat(d)); + } + + Plugin plugin = new Plugin(createInfo(name, name, "1"), null, null, + dependencySet); + + return createSmp(plugin); + } + + /** + * Method description + * + * + * @param names + * + * @return + * + * @throws IOException + */ + private List createSmps(String... names) throws IOException + { + List smps = Lists.newArrayList(); + + for (String name : names) + { + smps.add(createSmp(name)); + } + + return smps; + } + + /** + * Method description + * + * + * @param nodes + * + * @return + */ + private List unwrapIds(List nodes) + { + return Lists.transform(nodes, new Function() + { + + @Override + public String apply(PluginNode input) + { + return input.getId(); + } + }); + } + + //~--- fields --------------------------------------------------------------- + + /** Field description */ + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); +}