Merge pull request #1097 from scm-manager/feature/optional_dependency_annotation

Feature/optional dependency annotation
This commit is contained in:
Sebastian Sdorra
2020-04-15 11:11:37 +02:00
committed by GitHub
9 changed files with 124 additions and 321 deletions

View File

@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Changed
- Removed the `requires` attribute on the `@Extension` annotation and instead create a new `@Requires` annotation ([#1097](https://github.com/scm-manager/scm-manager/pull/1097))
## 2.0.0-rc7 - 2020-04-09
### Added
- Fire various plugin events ([#1088](https://github.com/scm-manager/scm-manager/pull/1088))

View File

@@ -21,138 +21,84 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.annotation;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Strings;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
//~--- JDK imports ------------------------------------------------------------
import java.util.Map;
import java.util.Map.Entry;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*/
public class ClassSetElement implements DescriptorElement
{
public class ClassSetElement implements DescriptorElement {
/** Field description */
private static final String EL_CLASS = "class";
/** Field description */
private static final String EL_DESCRIPTION = "description";
//~--- constructors ---------------------------------------------------------
private final String elementName;
private final Iterable<ClassWithAttributes> classes;
/**
* Constructs ...
*
*
* @param elementName
* @param classes
*/
public ClassSetElement(String elementName,
Iterable<ClassWithAttributes> classes)
{
public ClassSetElement(String elementName, Iterable<ClassWithAttributes> classes) {
this.elementName = elementName;
this.classes = classes;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param doc
* @param root
*/
@Override
public void append(Document doc, Element root)
{
for (ClassWithAttributes c : classes)
{
public void append(Document doc, Element root) {
for (ClassWithAttributes c : classes) {
Element element = doc.createElement(elementName);
Element classEl = doc.createElement(EL_CLASS);
classEl.setTextContent(c.className);
if (!Strings.isNullOrEmpty(c.description))
{
if (!Strings.isNullOrEmpty(c.description)) {
Element descriptionEl = doc.createElement(EL_DESCRIPTION);
descriptionEl.setTextContent(c.description);
element.appendChild(descriptionEl);
}
for (Entry<String, String> e : c.attributes.entrySet())
{
for (Entry<String, String> e : c.attributes.entrySet()) {
Element attr = doc.createElement(e.getKey());
attr.setTextContent(e.getValue());
element.appendChild(attr);
}
if (c.requires != null) {
for (String requiresEntry : c.requires) {
Element requiresElement = doc.createElement("requires");
requiresElement.setTextContent(requiresEntry);
element.appendChild(requiresElement);
}
}
element.appendChild(classEl);
root.appendChild(element);
}
}
//~--- inner classes --------------------------------------------------------
public static class ClassWithAttributes {
/**
* Class description
*
*
* @version Enter version here..., 14/03/18
* @author Enter your name here...
*/
public static class ClassWithAttributes
{
/**
* Constructs ...
*
*
* @param className
* @param description
* @param attributes
*/
public ClassWithAttributes(String className, String description,
Map<String, String> attributes)
{
this.className = className;
this.description = description;
this.attributes = attributes;
}
//~--- fields -------------------------------------------------------------
/** Field description */
private final String className;
private final String description;
private final String[] requires;
private final Map<String, String> attributes;
/** Field description */
private final String className;
/** Field description */
private final String description;
public ClassWithAttributes(String className, String description,
String[] requires, Map<String, String> attributes) {
this.className = className;
this.description = description;
this.requires = requires;
this.attributes = attributes;
}
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private final Iterable<ClassWithAttributes> classes;
/** Field description */
private final String elementName;
}

View File

@@ -42,6 +42,7 @@ import org.xml.sax.SAXException;
import sonia.scm.annotation.ClassSetElement.ClassWithAttributes;
import sonia.scm.plugin.PluginAnnotation;
import sonia.scm.plugin.Requires;
//~--- JDK imports ------------------------------------------------------------
@@ -247,6 +248,14 @@ public final class ScmAnnotationProcessor extends AbstractProcessor {
if (isClassOrInterface(e)) {
TypeElement type = (TypeElement) e;
String[] requires = null;
Requires requiresAnnotation = type.getAnnotation(Requires.class);
if (requiresAnnotation != null) {
requires = requiresAnnotation.value();
}
String desc = processingEnv.getElementUtils().getDocComment(type);
if (desc != null) {
@@ -255,7 +264,7 @@ public final class ScmAnnotationProcessor extends AbstractProcessor {
classes.add(
new ClassWithAttributes(
type.getQualifiedName().toString(), desc, getAttributesFromAnnotation(e, annotation)
type.getQualifiedName().toString(), desc, requires, getAttributesFromAnnotation(e, annotation)
)
);
}

View File

@@ -41,14 +41,4 @@ import java.lang.annotation.Target;
@PluginAnnotation("extension")
@Retention(RetentionPolicy.RUNTIME)
public @interface Extension {
/**
* This extension is loaded only if all of the specified plugins are installed.
* 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

@@ -21,30 +21,31 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.plugin;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
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;
/**
* The {@link Requires} annotation can be used to bind classes (e.g. Extensions, Rest Resources, etc.) only if certain
* plugins is installed. This is very useful in combination with optional plugin dependencies.
*
* @since 2.0.0
*/
@Documented
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface Requires {
@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<>();
/**
* The annotated class is loaded only if all of the specified plugins are installed.
* The value has to be an array of string with the plugin names the class should depend on.
*
* @return list of required plugins to load this class
*/
String[] value();
}

View File

@@ -21,133 +21,36 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.plugin;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
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;
//~--- JDK imports ------------------------------------------------------------
import java.util.HashSet;
import java.util.Set;
/**
*
* @author Sebastian Sdorra
* @since 2.0.0
*/
public final class ClassElement
{
/**
* Constructs ...
*
*/
ClassElement() {}
/**
* Constructs ...
*
*
* @param clazz
* @param description
*/
public ClassElement(Class<?> clazz, String description)
{
this.clazz = clazz;
this.description = description;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param obj
*
* @return
*/
@Override
public boolean equals(Object obj)
{
if (obj == null)
{
return false;
}
if (getClass() != obj.getClass())
{
return false;
}
final ClassElement other = (ClassElement) obj;
return Objects.equal(clazz, other.clazz)
&& Objects.equal(description, other.description);
}
/**
* Method description
*
*
* @return
*/
@Override
public int hashCode()
{
return Objects.hashCode(clazz, description);
}
/**
* Method description
*
*
* @return
*/
@Override
public String toString()
{
//J-
return MoreObjects.toStringHelper(this)
.add("clazz", clazz)
.add("description", description)
.toString();
//J+
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
public Class<?> getClazz()
{
return clazz;
}
/**
* Method description
*
*
* @return
*/
public String getDescription()
{
return description;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
@Getter
@NoArgsConstructor
@AllArgsConstructor
@ToString
@EqualsAndHashCode
@XmlAccessorType(XmlAccessType.FIELD)
public final class ClassElement {
@XmlElement(name = "class")
private Class<?> clazz;
/** Field description */
private String clazz;
private String description;
private Set<String> requires = new HashSet<>();
}

View File

@@ -48,10 +48,6 @@ import javax.xml.bind.annotation.XmlRootElement;
@XmlAccessorType(XmlAccessType.FIELD)
public class ScmModule
{
/** Field description */
private static final Unwrapper unwrapper = new Unwrapper();
//~--- get methods ----------------------------------------------------------
/**
@@ -60,9 +56,9 @@ public class ScmModule
*
* @return
*/
public Iterable<Class<?>> getEvents()
public Iterable<ClassElement> getEvents()
{
return unwrap(events);
return nonNull(events);
}
/**
@@ -82,7 +78,7 @@ public class ScmModule
*
* @return
*/
public Iterable<ExtensionElement> getExtensions()
public Iterable<ClassElement> getExtensions()
{
return nonNull(extensions);
}
@@ -93,9 +89,9 @@ public class ScmModule
*
* @return
*/
public Iterable<Class<?>> getRestProviders()
public Iterable<ClassElement> getRestProviders()
{
return unwrap(restProviders);
return nonNull(restProviders);
}
/**
@@ -104,9 +100,9 @@ public class ScmModule
*
* @return
*/
public Iterable<Class<?>> getRestResources()
public Iterable<ClassElement> getRestResources()
{
return unwrap(restResources);
return nonNull(restResources);
}
/**
@@ -152,57 +148,6 @@ public class ScmModule
return iterable;
}
/**
* Method description
*
*
* @param iterable
*
* @return
*/
private Iterable<Class<?>> unwrap(Iterable<ClassElement> iterable)
{
Iterable<Class<?>> unwrapped;
if (iterable != null)
{
unwrapped = Iterables.transform(iterable, unwrapper);
}
else
{
unwrapped = ImmutableSet.of();
}
return unwrapped;
}
//~--- inner classes --------------------------------------------------------
/**
* Class description
*
*
* @version Enter version here..., 14/03/28
* @author Enter your name here...
*/
private static class Unwrapper implements Function<ClassElement, Class<?>>
{
/**
* Method description
*
*
* @param classElement
*
* @return
*/
@Override
public Class<?> apply(ClassElement classElement)
{
return classElement.getClazz();
}
}
//~--- fields ---------------------------------------------------------------
@@ -216,7 +161,7 @@ public class ScmModule
/** Field description */
@XmlElement(name = "extension")
private Set<ExtensionElement> extensions;
private Set<ClassElement> extensions;
/** Field description */
@XmlElement(name = "rest-provider")

View File

@@ -38,6 +38,7 @@ import static org.junit.Assert.*;
//~--- JDK imports ------------------------------------------------------------
import java.net.URL;
import java.util.Collections;
import javax.xml.bind.JAXB;
@@ -63,7 +64,7 @@ public class ScmModuleTest
//J-
assertThat(
Iterables.transform(module.getExtensions(), ExtensionElement::getClazz),
Iterables.transform(module.getExtensions(), ClassElement::getClazz),
containsInAnyOrder(
String.class.getName(),
Integer.class.getName()
@@ -78,11 +79,8 @@ public class ScmModuleTest
)
);
assertThat(
module.getEvents(),
containsInAnyOrder(
String.class,
Boolean.class
)
module.getEvents().iterator().next(),
instanceOf(ClassElement.class)
);
assertThat(
module.getSubscribers(),
@@ -92,18 +90,12 @@ public class ScmModuleTest
)
);
assertThat(
module.getRestProviders(),
containsInAnyOrder(
Integer.class,
Long.class
)
module.getRestProviders().iterator().next(),
instanceOf(ClassElement.class)
);
assertThat(
module.getRestResources(),
containsInAnyOrder(
Float.class,
Double.class
)
module.getRestResources().iterator().next(),
instanceOf(ClassElement.class)
);
//J+
}

View File

@@ -39,6 +39,7 @@ import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
@@ -60,10 +61,10 @@ public final class ExtensionCollector
this.pluginIndex = createPluginIndex(installedPlugins);
for (ScmModule module : modules) {
collectRootElements(module);
collectRootElements(moduleClassLoader, module);
}
for (ScmModule plugin : PluginsInternal.unwrap(installedPlugins)) {
collectRootElements(plugin);
for (InstalledPlugin plugin : installedPlugins) {
collectRootElements(plugin.getClassLoader(), plugin.getDescriptor());
}
for (ScmModule module : modules) {
@@ -252,7 +253,7 @@ public final class ExtensionCollector
}
private void collectExtensions(ClassLoader defaultClassLoader, ScmModule module) {
for (ExtensionElement extension : module.getExtensions()) {
for (ClassElement extension : module.getExtensions()) {
if (isRequirementFulfilled(extension)) {
Class<?> extensionClass = loadExtension(defaultClassLoader, extension);
appendExtension(extensionClass);
@@ -260,7 +261,18 @@ public final class ExtensionCollector
}
}
private Class<?> loadExtension(ClassLoader classLoader, ExtensionElement extension) {
private Set<Class<?>> collectClasses(ClassLoader defaultClassLoader, Iterable<ClassElement> classElements) {
Set<Class<?>> classes = new HashSet<>();
for (ClassElement element : classElements) {
if (isRequirementFulfilled(element)) {
Class<?> loadedClass = loadExtension(defaultClassLoader, element);
classes.add(loadedClass);
}
}
return classes;
}
private Class<?> loadExtension(ClassLoader classLoader, ClassElement extension) {
try {
return classLoader.loadClass(extension.getClazz());
} catch (ClassNotFoundException ex) {
@@ -268,7 +280,7 @@ public final class ExtensionCollector
}
}
private boolean isRequirementFulfilled(ExtensionElement extension) {
private boolean isRequirementFulfilled(ClassElement extension) {
if (extension.getRequires() != null) {
for (String requiredPlugin : extension.getRequires()) {
if (!pluginIndex.contains(requiredPlugin)) {
@@ -286,15 +298,15 @@ public final class ExtensionCollector
*
* @param module
*/
private void collectRootElements(ScmModule module)
private void collectRootElements(ClassLoader classLoader, ScmModule module)
{
for (ExtensionPointElement epe : module.getExtensionPoints())
{
extensionPointIndex.put(epe.getClazz(), epe);
}
restProviders.addAll(Lists.newArrayList(module.getRestProviders()));
restResources.addAll(Lists.newArrayList(module.getRestResources()));
restProviders.addAll(collectClasses(classLoader, module.getRestProviders()));
restResources.addAll(collectClasses(classLoader, module.getRestResources()));
Iterables.addAll(webElements, module.getWebElements());
}