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:
Sebastian Sdorra
2020-01-08 14:27:11 +01:00
parent 3244e552a9
commit c1aa4af6e0
14 changed files with 262 additions and 86 deletions

View File

@@ -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 -->

View File

@@ -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 {};
}

View File

@@ -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<>();
}

View File

@@ -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;

View File

@@ -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")

View File

@@ -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(

View File

@@ -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 -->

View File

@@ -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 ----------------------------------------------------------
/**

View File

@@ -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())
{

View File

@@ -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
*

View File

@@ -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();

View File

@@ -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 ---------------------------------------------------------------

View File

@@ -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);
}

View File

@@ -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);
}