mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-07-04 15:58:06 +02:00
Merge pull request #1097 from scm-manager/feature/optional_dependency_annotation
Feature/optional dependency annotation
This commit is contained in:
@@ -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))
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {};
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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<>();
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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+
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user