mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-10 15:35:49 +01:00
implemented optional dependencies
Plugin authors could now define optional dependencies to other plugins in their pom. Extensions which are using classes from optional dependencies must specify this with the "requires" attribute of the extension annotation. Extensions with "requires" attribute are not installed if one of the specified plugins, is not installed.
This commit is contained in:
@@ -23,7 +23,7 @@
|
||||
-->
|
||||
|
||||
<!--- root element of the plugin descriptor -->
|
||||
<!ELEMENT plugin (scm-version|information|child-first-classloader|conditions|resources|dependencies|extension|extension-point|rest-resource|subscriber)*>
|
||||
<!ELEMENT plugin (scm-version|information|child-first-classloader|conditions|resources|dependencies|optional-dependencies|extension|extension-point|rest-resource|subscriber)*>
|
||||
|
||||
<!--- major scm-manager version -->
|
||||
<!ELEMENT scm-version (#PCDATA)>
|
||||
@@ -79,7 +79,10 @@
|
||||
<!--- contains plugin dependencies -->
|
||||
<!ELEMENT dependencies (dependency)*>
|
||||
|
||||
<!--- sing plugin dependency -->
|
||||
<!--- contains optional plugin dependencies -->
|
||||
<!ELEMENT optional-dependencies (dependency)*>
|
||||
|
||||
<!--- single plugin dependency -->
|
||||
<!ELEMENT dependency (#PCDATA)>
|
||||
|
||||
<!-- generated entries -->
|
||||
|
||||
@@ -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 {};
|
||||
}
|
||||
|
||||
@@ -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<String> requires = new HashSet<>();
|
||||
}
|
||||
@@ -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<String> dependencies)
|
||||
boolean childFirstClassLoader, Set<String> dependencies, Set<String> 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<String> getOptionalDependencies() {
|
||||
if (optionalDependencies == null)
|
||||
{
|
||||
optionalDependencies = ImmutableSet.of();
|
||||
}
|
||||
|
||||
return optionalDependencies;
|
||||
}
|
||||
|
||||
public Set<String> 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<String> dependencies;
|
||||
|
||||
/** Field description */
|
||||
@XmlElement(name = "dependency")
|
||||
@XmlElementWrapper(name = "optional-dependencies")
|
||||
private Set<String> optionalDependencies;
|
||||
|
||||
/** Field description */
|
||||
@XmlElement(name = "information")
|
||||
private PluginInformation information;
|
||||
|
||||
@@ -89,9 +89,9 @@ public class ScmModule
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Iterable<Class<?>> getExtensions()
|
||||
public Iterable<ExtensionElement> getExtensions()
|
||||
{
|
||||
return unwrap(extensions);
|
||||
return nonNull(extensions);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -223,7 +223,7 @@ public class ScmModule
|
||||
|
||||
/** Field description */
|
||||
@XmlElement(name = "extension")
|
||||
private Set<ClassElement> extensions;
|
||||
private Set<ExtensionElement> extensions;
|
||||
|
||||
/** Field description */
|
||||
@XmlElement(name = "rest-provider")
|
||||
|
||||
@@ -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,10 +70,10 @@ 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(
|
||||
|
||||
@@ -47,10 +47,14 @@
|
||||
|
||||
<extension>
|
||||
<class>java.lang.String</class>
|
||||
<requires>scm-mail-plugin</requires>
|
||||
<requires>scm-review-plugin</requires>
|
||||
<description>ext01</description>
|
||||
</extension>
|
||||
|
||||
<extension>
|
||||
<class>java.lang.Integer</class>
|
||||
<description>ext02</description>
|
||||
</extension>
|
||||
|
||||
<!-- rest providers -->
|
||||
|
||||
@@ -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<InstalledPluginDescriptor> unwrap()
|
||||
{
|
||||
return PluginsInternal.unwrap(installedPlugins);
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
|
||||
@@ -98,8 +98,8 @@ public final class ExplodedSmp implements Comparable<ExplodedSmp>
|
||||
{
|
||||
int result;
|
||||
|
||||
Set<String> depends = plugin.getDependencies();
|
||||
Set<String> odepends = o.plugin.getDependencies();
|
||||
Set<String> depends = plugin.getDependenciesInclusiveOptionals();
|
||||
Set<String> odepends = o.plugin.getDependenciesInclusiveOptionals();
|
||||
|
||||
if (depends.isEmpty() && odepends.isEmpty())
|
||||
{
|
||||
|
||||
@@ -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<ScmModule> modules)
|
||||
{
|
||||
for (ScmModule module : modules)
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ExtensionCollector.class);
|
||||
|
||||
private final Set<String> pluginIndex;
|
||||
|
||||
public ExtensionCollector(ClassLoader moduleClassLoader, Set<ScmModule> modules, Set<InstalledPlugin> 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<String> createPluginIndex(Set<InstalledPlugin> 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
|
||||
*
|
||||
|
||||
@@ -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<PluginNode> rootNodes = pluginTree.getRootNodes();
|
||||
|
||||
|
||||
@@ -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<String> 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 ---------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<String>(), "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();
|
||||
@@ -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<PluginNode> 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<PluginNode> 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<PluginNode> 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<String> dependencies, Set<String> optionalDependencies) throws IOException {
|
||||
InstalledPluginDescriptor plugin = new InstalledPluginDescriptor(
|
||||
2,
|
||||
createInfo(name, "1"),
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
dependencies,
|
||||
optionalDependencies
|
||||
);
|
||||
return createSmp(plugin);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user