diff --git a/docs/dtd/plugin/2.0.0-01.dtd b/docs/dtd/plugin/2.0.0-01.dtd index 954a2c2219..62907f9481 100644 --- a/docs/dtd/plugin/2.0.0-01.dtd +++ b/docs/dtd/plugin/2.0.0-01.dtd @@ -23,7 +23,7 @@ --> - + @@ -79,7 +79,10 @@ - + + + + diff --git a/scm-annotations/src/main/java/sonia/scm/plugin/Extension.java b/scm-annotations/src/main/java/sonia/scm/plugin/Extension.java index ce3bd24406..948dd46200 100644 --- a/scm-annotations/src/main/java/sonia/scm/plugin/Extension.java +++ b/scm-annotations/src/main/java/sonia/scm/plugin/Extension.java @@ -49,4 +49,15 @@ import java.lang.annotation.Target; @Target({ ElementType.TYPE }) @PluginAnnotation("extension") @Retention(RetentionPolicy.RUNTIME) -public @interface Extension {} +public @interface Extension { + /** + * List of required plugins to load this extension. + * The requires attribute can be used to implement optional extensions. + * A plugin author is able to implement an extension point of an optional plugin and the extension is only loaded if + * all of the specified plugins are installed. + * + * @since 2.0.0 + * @return list of required plugins to load this extension + */ + String[] requires() default {}; +} diff --git a/scm-core/src/main/java/sonia/scm/plugin/ExtensionElement.java b/scm-core/src/main/java/sonia/scm/plugin/ExtensionElement.java new file mode 100644 index 0000000000..1dd6f51291 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/plugin/ExtensionElement.java @@ -0,0 +1,26 @@ +package sonia.scm.plugin; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import java.util.HashSet; +import java.util.Set; + +@Getter +@ToString +@NoArgsConstructor +@EqualsAndHashCode +@AllArgsConstructor +@XmlAccessorType(XmlAccessType.FIELD) +public class ExtensionElement { + @XmlElement(name = "class") + private String clazz; + private String description; + private Set requires = new HashSet<>(); +} diff --git a/scm-core/src/main/java/sonia/scm/plugin/InstalledPluginDescriptor.java b/scm-core/src/main/java/sonia/scm/plugin/InstalledPluginDescriptor.java index 88ae4c5dff..097a06ab2c 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/InstalledPluginDescriptor.java +++ b/scm-core/src/main/java/sonia/scm/plugin/InstalledPluginDescriptor.java @@ -38,6 +38,7 @@ package sonia.scm.plugin; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; @@ -76,7 +77,7 @@ public final class InstalledPluginDescriptor extends ScmModule implements Plugin */ public InstalledPluginDescriptor(int scmVersion, PluginInformation information, PluginResources resources, PluginCondition condition, - boolean childFirstClassLoader, Set dependencies) + boolean childFirstClassLoader, Set dependencies, Set optionalDependencies) { this.scmVersion = scmVersion; this.information = information; @@ -84,6 +85,7 @@ public final class InstalledPluginDescriptor extends ScmModule implements Plugin this.condition = condition; this.childFirstClassLoader = childFirstClassLoader; this.dependencies = dependencies; + this.optionalDependencies = optionalDependencies; } //~--- methods -------------------------------------------------------------- @@ -116,7 +118,8 @@ public final class InstalledPluginDescriptor extends ScmModule implements Plugin && Objects.equal(information, other.information) && Objects.equal(resources, other.resources) && Objects.equal(childFirstClassLoader, other.childFirstClassLoader) - && Objects.equal(dependencies, other.dependencies); + && Objects.equal(dependencies, other.dependencies) + && Objects.equal(optionalDependencies, other.optionalDependencies); } /** @@ -129,7 +132,7 @@ public final class InstalledPluginDescriptor extends ScmModule implements Plugin public int hashCode() { return Objects.hashCode(scmVersion, condition, information, resources, - childFirstClassLoader, dependencies); + childFirstClassLoader, dependencies, optionalDependencies); } /** @@ -149,6 +152,7 @@ public final class InstalledPluginDescriptor extends ScmModule implements Plugin .add("resources", resources) .add("childFirstClassLoader", childFirstClassLoader) .add("dependencies", dependencies) + .add("optionalDependencies", optionalDependencies) .toString(); //J+ } @@ -186,6 +190,27 @@ public final class InstalledPluginDescriptor extends ScmModule implements Plugin return dependencies; } + /** + * Method description + * + * + * @return + * + * @since 2.0.0 + */ + public Set getOptionalDependencies() { + if (optionalDependencies == null) + { + optionalDependencies = ImmutableSet.of(); + } + + return optionalDependencies; + } + + public Set getDependenciesInclusiveOptionals() { + return ImmutableSet.copyOf(Iterables.concat(getDependencies(), getOptionalDependencies())); + } + /** * Method description * @@ -246,6 +271,11 @@ public final class InstalledPluginDescriptor extends ScmModule implements Plugin @XmlElementWrapper(name = "dependencies") private Set dependencies; + /** Field description */ + @XmlElement(name = "dependency") + @XmlElementWrapper(name = "optional-dependencies") + private Set optionalDependencies; + /** Field description */ @XmlElement(name = "information") private PluginInformation information; diff --git a/scm-core/src/main/java/sonia/scm/plugin/ScmModule.java b/scm-core/src/main/java/sonia/scm/plugin/ScmModule.java index 683b2a7e0c..398698273c 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/ScmModule.java +++ b/scm-core/src/main/java/sonia/scm/plugin/ScmModule.java @@ -89,9 +89,9 @@ public class ScmModule * * @return */ - public Iterable> getExtensions() + public Iterable getExtensions() { - return unwrap(extensions); + return nonNull(extensions); } /** @@ -223,7 +223,7 @@ public class ScmModule /** Field description */ @XmlElement(name = "extension") - private Set extensions; + private Set extensions; /** Field description */ @XmlElement(name = "rest-provider") diff --git a/scm-core/src/test/java/sonia/scm/plugin/ScmModuleTest.java b/scm-core/src/test/java/sonia/scm/plugin/ScmModuleTest.java index 1629562132..86bd7fe19d 100644 --- a/scm-core/src/test/java/sonia/scm/plugin/ScmModuleTest.java +++ b/scm-core/src/test/java/sonia/scm/plugin/ScmModuleTest.java @@ -33,6 +33,7 @@ package sonia.scm.plugin; //~--- non-JDK imports -------------------------------------------------------- +import com.google.common.collect.Iterables; import com.google.common.io.Resources; import org.junit.Test; @@ -69,43 +70,43 @@ public class ScmModuleTest //J- assertThat( - module.getExtensions(), + Iterables.transform(module.getExtensions(), ExtensionElement::getClazz), containsInAnyOrder( - String.class, - Integer.class + String.class.getName(), + Integer.class.getName() ) ); assertThat( - module.getExtensionPoints(), + module.getExtensionPoints(), containsInAnyOrder( - new ExtensionPointElement(String.class, "ext01", true, true), - new ExtensionPointElement(Long.class, "ext02", true, true), + new ExtensionPointElement(String.class, "ext01", true, true), + new ExtensionPointElement(Long.class, "ext02", true, true), new ExtensionPointElement(Integer.class, "ext03", false, true) ) ); assertThat( - module.getEvents(), + module.getEvents(), containsInAnyOrder( String.class, Boolean.class ) ); assertThat( - module.getSubscribers(), + module.getSubscribers(), containsInAnyOrder( - new SubscriberElement(Long.class, Integer.class, "sub01"), + new SubscriberElement(Long.class, Integer.class, "sub01"), new SubscriberElement(Double.class, Float.class, "sub02") ) ); assertThat( - module.getRestProviders(), + module.getRestProviders(), containsInAnyOrder( Integer.class, Long.class ) ); assertThat( - module.getRestResources(), + module.getRestResources(), containsInAnyOrder( Float.class, Double.class diff --git a/scm-core/src/test/resources/sonia/scm/plugin/module.xml b/scm-core/src/test/resources/sonia/scm/plugin/module.xml index c135803a7d..0bea44d06e 100644 --- a/scm-core/src/test/resources/sonia/scm/plugin/module.xml +++ b/scm-core/src/test/resources/sonia/scm/plugin/module.xml @@ -1,6 +1,6 @@ - + java.lang.Long @@ -47,28 +47,32 @@ java.lang.String + scm-mail-plugin + scm-review-plugin + ext01 java.lang.Integer + ext02 - + - + java.lang.Integer - + java.lang.Long - + - + java.lang.Float - + java.lang.Double 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 5612b0395c..e5ab89821f 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginLoader.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginLoader.java @@ -99,7 +99,7 @@ public class DefaultPluginLoader implements PluginLoader modules = getInstalled(parent, context, PATH_MODULECONFIG); - collector = new ExtensionCollector(Iterables.concat(modules, unwrap())); + collector = new ExtensionCollector(parent, modules, installedPlugins); extensionProcessor = new DefaultExtensionProcessor(collector); } catch (IOException | JAXBException ex) @@ -170,19 +170,6 @@ public class DefaultPluginLoader implements PluginLoader return uberWebResourceLoader; } - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - private Iterable unwrap() - { - return PluginsInternal.unwrap(installedPlugins); - } - //~--- get methods ---------------------------------------------------------- /** diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/ExplodedSmp.java b/scm-webapp/src/main/java/sonia/scm/plugin/ExplodedSmp.java index ee2514dc3e..60bcc09dcb 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/ExplodedSmp.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/ExplodedSmp.java @@ -98,8 +98,8 @@ public final class ExplodedSmp implements Comparable { int result; - Set depends = plugin.getDependencies(); - Set odepends = o.plugin.getDependencies(); + Set depends = plugin.getDependenciesInclusiveOptionals(); + Set odepends = o.plugin.getDependenciesInclusiveOptionals(); if (depends.isEmpty() && odepends.isEmpty()) { diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/ExtensionCollector.java b/scm-webapp/src/main/java/sonia/scm/plugin/ExtensionCollector.java index 9ecf9a9da8..8ddf847935 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/ExtensionCollector.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/ExtensionCollector.java @@ -39,6 +39,8 @@ import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; //~--- JDK imports ------------------------------------------------------------ @@ -47,6 +49,7 @@ import java.util.Collections; import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.stream.Collectors; /** * @@ -56,23 +59,33 @@ import java.util.Set; public final class ExtensionCollector { - /** - * Constructs ... - * - * - * @param modules - */ - ExtensionCollector(Iterable modules) - { - for (ScmModule module : modules) - { + private static final Logger LOG = LoggerFactory.getLogger(ExtensionCollector.class); + + private final Set pluginIndex; + + public ExtensionCollector(ClassLoader moduleClassLoader, Set modules, Set installedPlugins) { + this.pluginIndex = createPluginIndex(installedPlugins); + + for (ScmModule module : modules) { collectRootElements(module); } - - for (ScmModule module : modules) - { - collectExtensions(module); + for (ScmModule plugin : PluginsInternal.unwrap(installedPlugins)) { + collectRootElements(plugin); } + + for (ScmModule module : modules) { + collectExtensions(moduleClassLoader, module); + } + + for (InstalledPlugin plugin : installedPlugins) { + collectExtensions(plugin.getClassLoader(), plugin.getDescriptor()); + } + } + + private Set createPluginIndex(Set installedPlugins) { + return installedPlugins.stream() + .map(p -> p.getDescriptor().getInformation().getName()) + .collect(Collectors.toSet()); } //~--- methods -------------------------------------------------------------- @@ -245,20 +258,35 @@ public final class ExtensionCollector } } - /** - * Method description - * - * - * @param module - */ - private void collectExtensions(ScmModule module) - { - for (Class extension : module.getExtensions()) - { - appendExtension(extension); + private void collectExtensions(ClassLoader defaultClassLoader, ScmModule module) { + for (ExtensionElement extension : module.getExtensions()) { + if (isRequirementFulfilled(extension)) { + Class extensionClass = loadExtension(defaultClassLoader, extension); + appendExtension(extensionClass); + } } } + private Class loadExtension(ClassLoader classLoader, ExtensionElement extension) { + try { + return classLoader.loadClass(extension.getClazz()); + } catch (ClassNotFoundException ex) { + throw new RuntimeException("failed to load clazz", ex); + } + } + + private boolean isRequirementFulfilled(ExtensionElement extension) { + if (extension.getRequires() != null) { + for (String requiredPlugin : extension.getRequires()) { + if (!pluginIndex.contains(requiredPlugin)) { + LOG.debug("skip loading of extension {}, because the required plugin {} is not installed", extension.getClazz(), requiredPlugin); + return false; + } + } + } + return true; + } + /** * Method description * 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 2bbab3c650..431cb44e1b 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java @@ -189,7 +189,7 @@ public final class PluginProcessor PluginTree pluginTree = new PluginTree(smps); - logger.trace("build plugin tree: {}", pluginTree); + logger.info("install plugin tree:\n{}", pluginTree); List rootNodes = pluginTree.getRootNodes(); diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginTree.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginTree.java index bd338c0741..2ebe06db02 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PluginTree.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginTree.java @@ -35,15 +35,13 @@ package sonia.scm.plugin; 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; + +//~--- JDK imports ------------------------------------------------------------ /** * @@ -104,9 +102,7 @@ public final class PluginTree if ((condition == null) || condition.isSupported()) { - Set dependencies = plugin.getDependencies(); - - if ((dependencies == null) || dependencies.isEmpty()) + if (plugin.getDependencies().isEmpty() && plugin.getOptionalDependencies().isEmpty()) { rootNodes.add(new PluginNode(smp)); } @@ -170,6 +166,20 @@ public final class PluginTree //J+ } } + + boolean rootNode = smp.getPlugin().getDependencies().isEmpty(); + for (String dependency : smp.getPlugin().getOptionalDependencies()) { + if (appendNode(rootNodes, child, dependency)) { + rootNode = false; + } else { + logger.info("optional dependency {} of {} is not installed", dependency, child.getId()); + } + } + + if (rootNode) { + logger.info("could not find optional dependencies of {}, append it as root node", child.getId()); + rootNodes.add(new PluginNode(smp)); + } } /** @@ -212,7 +222,18 @@ public final class PluginTree @Override public String toString() { - return "plugin tree: " + rootNodes.toString(); + StringBuilder buffer = new StringBuilder(); + for (PluginNode node : rootNodes) { + append(buffer, "", node); + } + return buffer.toString(); + } + + private void append(StringBuilder buffer, String indent, PluginNode node) { + buffer.append(indent).append("+- ").append(node.getId()).append("\n"); + for (PluginNode child : node.getChildren()) { + append(buffer, indent + " ", child); + } } //~--- fields --------------------------------------------------------------- diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/ExplodedSmpTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/ExplodedSmpTest.java index 090d903f49..c4a4c0a0d3 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/ExplodedSmpTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/ExplodedSmpTest.java @@ -134,7 +134,7 @@ public class ExplodedSmpTest info.setVersion(version); InstalledPluginDescriptor plugin = new InstalledPluginDescriptor(2, info, null, null, false, - Sets.newSet(dependencies)); + Sets.newSet(dependencies), null); return new ExplodedSmp(null, plugin); } diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginTreeTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginTreeTest.java index 72c48d4d16..33a139aa5a 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/PluginTreeTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginTreeTest.java @@ -34,6 +34,7 @@ package sonia.scm.plugin; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.base.Function; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import org.junit.Rule; @@ -72,7 +73,7 @@ public class PluginTreeTest PluginCondition condition = new PluginCondition("999", new ArrayList(), "hit"); InstalledPluginDescriptor plugin = new InstalledPluginDescriptor(2, createInfo("a", "1"), null, condition, - false, null); + false, null, null); ExplodedSmp smp = createSmp(plugin); new PluginTree(smp).getRootNodes(); @@ -115,7 +116,7 @@ public class PluginTreeTest public void testScmVersion() throws IOException { InstalledPluginDescriptor plugin = new InstalledPluginDescriptor(1, createInfo("a", "1"), null, null, false, - null); + null, null); ExplodedSmp smp = createSmp(plugin); new PluginTree(smp).getRootNodes(); @@ -131,10 +132,10 @@ public class PluginTreeTest public void testSimpleDependencies() throws IOException { //J- - ExplodedSmp[] smps = new ExplodedSmp[] { + ExplodedSmp[] smps = new ExplodedSmp[] { createSmpWithDependency("a"), createSmpWithDependency("b", "a"), - createSmpWithDependency("c", "a", "b") + createSmpWithDependency("c", "a", "b") }; //J+ @@ -152,6 +153,61 @@ public class PluginTreeTest assertThat(unwrapIds(b.getChildren()), containsInAnyOrder("c")); } + @Test + public void testWithOptionalDependency() throws IOException { + ExplodedSmp[] smps = new ExplodedSmp[] { + createSmpWithDependency("a"), + createSmpWithDependency("b", null, ImmutableSet.of("a")), + createSmpWithDependency("c", null, ImmutableSet.of("a", "b")) + }; + + PluginTree tree = new PluginTree(smps); + List rootNodes = tree.getRootNodes(); + + assertThat(unwrapIds(rootNodes), containsInAnyOrder("a")); + + PluginNode a = rootNodes.get(0); + assertThat(unwrapIds(a.getChildren()), containsInAnyOrder("b", "c")); + + PluginNode b = a.getChild("b"); + assertThat(unwrapIds(b.getChildren()), containsInAnyOrder("c")); + } + + @Test + public void testWithDeepOptionalDependency() throws IOException { + ExplodedSmp[] smps = new ExplodedSmp[] { + createSmpWithDependency("a"), + createSmpWithDependency("b", "a"), + createSmpWithDependency("c", null, ImmutableSet.of("b")) + }; + + PluginTree tree = new PluginTree(smps); + + System.out.println(tree); + + List rootNodes = tree.getRootNodes(); + + assertThat(unwrapIds(rootNodes), containsInAnyOrder("a")); + + PluginNode a = rootNodes.get(0); + assertThat(unwrapIds(a.getChildren()), containsInAnyOrder("b")); + + PluginNode b = a.getChild("b"); + assertThat(unwrapIds(b.getChildren()), containsInAnyOrder("c")); + } + + @Test + public void testWithNonExistentOptionalDependency() throws IOException { + ExplodedSmp[] smps = new ExplodedSmp[] { + createSmpWithDependency("a", null, ImmutableSet.of("b")) + }; + + PluginTree tree = new PluginTree(smps); + List rootNodes = tree.getRootNodes(); + + assertThat(unwrapIds(rootNodes), containsInAnyOrder("a")); + } + /** * Method description * @@ -200,7 +256,7 @@ public class PluginTreeTest private ExplodedSmp createSmp(String name) throws IOException { return createSmp(new InstalledPluginDescriptor(2, createInfo(name, "1.0.0"), null, null, - false, null)); + false, null, null)); } /** @@ -224,10 +280,19 @@ public class PluginTreeTest { dependencySet.add(d); } + return createSmpWithDependency(name, dependencySet, null); + } - InstalledPluginDescriptor plugin = new InstalledPluginDescriptor(2, createInfo(name, "1"), null, null, - false, dependencySet); - + private ExplodedSmp createSmpWithDependency(String name, Set dependencies, Set optionalDependencies) throws IOException { + InstalledPluginDescriptor plugin = new InstalledPluginDescriptor( + 2, + createInfo(name, "1"), + null, + null, + false, + dependencies, + optionalDependencies + ); return createSmp(plugin); }