You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ra...@apache.org on 2020/03/20 10:33:05 UTC

[sling-scriptingbundle-maven-plugin] branch master updated: SLING-9208 - Generate provide capabilities based on the search paths

This is an automated email from the ASF dual-hosted git repository.

radu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-scriptingbundle-maven-plugin.git


The following commit(s) were added to refs/heads/master by this push:
     new f32eaf3  SLING-9208 - Generate provide capabilities based on the search paths
f32eaf3 is described below

commit f32eaf39b252a9ae77e8f3a2953c1f46b1170652
Author: Radu Cotescu <ra...@apache.org>
AuthorDate: Fri Mar 20 11:32:49 2020 +0100

    SLING-9208 - Generate provide capabilities based on the search paths
    
    * if scripts are placed in search path folders two resource types will
    be generated - an absolute and a relative one
---
 pom.xml                                            |   6 +
 .../scriptingbundle/maven/plugin/MetadataMojo.java |  76 ++++++---
 .../maven/plugin/ProvidedCapability.java           |  51 +++---
 .../maven/plugin/ResourceTypeFolderAnalyser.java   | 172 ++++++++-------------
 .../maven/plugin/MetadataMojoTest.java             |  97 +++++++-----
 .../src/main/scripts/libs/sling/test/test.html     |  18 +++
 src/test/resources/project-2/pom.xml               |  54 +++++++
 .../src/main/scripts/libs/sling/test/test.html     |  18 +++
 8 files changed, 313 insertions(+), 179 deletions(-)

diff --git a/pom.xml b/pom.xml
index ffe6217..410e569 100644
--- a/pom.xml
+++ b/pom.xml
@@ -189,6 +189,12 @@
             <version>3.4</version>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.scripting.bundle.tracker</artifactId>
+            <scope>compile</scope>
+            <version>0.1.1-SNAPSHOT</version>
+        </dependency>
 
         <dependency>
             <groupId>org.osgi</groupId>
diff --git a/src/main/java/org/apache/sling/scriptingbundle/maven/plugin/MetadataMojo.java b/src/main/java/org/apache/sling/scriptingbundle/maven/plugin/MetadataMojo.java
index 2a37d82..107a906 100644
--- a/src/main/java/org/apache/sling/scriptingbundle/maven/plugin/MetadataMojo.java
+++ b/src/main/java/org/apache/sling/scriptingbundle/maven/plugin/MetadataMojo.java
@@ -55,8 +55,7 @@ import org.osgi.framework.VersionRange;
       defaultPhase = LifecyclePhase.PACKAGE)
 public class MetadataMojo extends AbstractMojo {
 
-    @Parameter(defaultValue = "${project}",
-               readonly = true)
+    @Parameter(defaultValue = "${project}", readonly = true, required = true)
     private MavenProject project;
 
     /**
@@ -99,11 +98,36 @@ public class MetadataMojo extends AbstractMojo {
     @Parameter
     private Map<String, String> scriptEngineMappings;
 
+    /**
+     * Allows overriding the default search paths ({@code /apps} and {@code /libs}). When scripts are organised in folders which follow
+     * the search path structure, the Mojo will generate two resource types for each resource type folder. For example:
+     * <pre>
+     *     src/main/scripts/apps/org/apache/sling/example/example.html
+     * </pre>
+     * will generate the following two resource types
+     * <pre>
+     *     org/apache/sling/example
+     *     /apps/org/apache/sling/example
+     * </pre>
+     *
+     * However, the following script
+     * <pre>
+     *     src/main/scripts/org/apache/sling/example/example.html
+     * </pre>
+     * will generate only one resource type
+     * <pre>
+     *     org/apache/sling/example
+     * </pre>
+     */
+    @Parameter(property = "scriptingbundle.searchPaths")
+    private Set<String> searchPaths;
+
     static final Set<String> METHODS = new HashSet<>(Arrays.asList("TRACE", "OPTIONS", "GET", "HEAD", "POST", "PUT",
             "DELETE", "PATCH"));
     static final String EXTENDS_FILE = "extends";
     static final String REQUIRES_FILE = "requires";
     static final String CAPABILITY_NS = "sling.resourceType";
+    static final String CAPABILITY_RESOURCE_TYPE_AT = "sling.resourceType:List<String>";
     static final String CAPABILITY_SELECTORS_AT = CAPABILITY_NS + ".selectors:List<String>";
     static final String CAPABILITY_EXTENSIONS_AT = CAPABILITY_NS + ".extensions:List<String>";
     static final String CAPABILITY_METHODS_AT = "sling.servlet.methods:List<String>";
@@ -111,6 +135,7 @@ public class MetadataMojo extends AbstractMojo {
     static final String CAPABILITY_EXTENDS_AT = "extends";
     static final String CAPABILITY_SCRIPT_ENGINE_AT = "scriptEngine";
     static final Map<String, String> DEFAULT_EXTENSION_TO_SCRIPT_ENGINE_MAPPING;
+    static final Set<String> DEFAULT_SEARCH_PATHS;
 
     static {
         DEFAULT_EXTENSION_TO_SCRIPT_ENGINE_MAPPING = new HashMap<>();
@@ -127,6 +152,8 @@ public class MetadataMojo extends AbstractMojo {
          * commented out since Thymeleaf uses the same 'html' extension like HTL
          */
 //        DEFAULT_EXTENSION_TO_SCRIPT_ENGINE_MAPPING.put("html", "thymeleaf");
+
+        DEFAULT_SEARCH_PATHS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("/libs", "/apps")));
     }
 
     private Capabilities capabilities;
@@ -158,7 +185,12 @@ public class MetadataMojo extends AbstractMojo {
         if (scriptEngineMappings != null) {
             mappings.putAll(scriptEngineMappings);
         }
-        capabilities = generateCapabilities(root, resourceTypeDirectories, mappings);
+        scriptEngineMappings = mappings;
+        Set<String> searchPathsSet;
+        if (searchPaths == null || searchPaths.isEmpty()) {
+            searchPaths = DEFAULT_SEARCH_PATHS;
+        }
+        capabilities = generateCapabilities(root, resourceTypeDirectories);
         String providedCapabilitiesDefinition = getProvidedCapabilitiesString(capabilities);
         String requiredCapabilitiesDefinition = getRequiredCapabilitiesString(capabilities);
         project.getProperties().put(this.getClass().getPackage().getName() + "." + Constants.PROVIDE_CAPABILITY,
@@ -168,13 +200,13 @@ public class MetadataMojo extends AbstractMojo {
     }
 
     @NotNull
-    private Capabilities generateCapabilities(@NotNull String root, @NotNull List<Path> resourceTypeDirectories, @NotNull Map<String,
-            String> scriptEngineMappings) {
-        ResourceTypeFolderAnalyser analyser = new ResourceTypeFolderAnalyser(getLog(), Paths.get(root));
+    private Capabilities generateCapabilities(@NotNull String root, @NotNull List<Path> resourceTypeDirectories) {
+        ResourceTypeFolderAnalyser analyser = new ResourceTypeFolderAnalyser(getLog(), Paths.get(root), scriptEngineMappings,
+                searchPaths);
         Set<ProvidedCapability> providedCapabilities = new LinkedHashSet<>();
         Set<RequiredCapability> requiredCapabilities = new LinkedHashSet<>();
         for (Path resourceTypeDirectory : resourceTypeDirectories) {
-            Capabilities resourceTypeCapabilities = analyser.getCapabilities(resourceTypeDirectory, scriptEngineMappings);
+            Capabilities resourceTypeCapabilities = analyser.getCapabilities(resourceTypeDirectory);
             providedCapabilities.addAll(resourceTypeCapabilities.getProvidedCapabilities());
             requiredCapabilities.addAll(resourceTypeCapabilities.getRequiredCapabilities());
         }
@@ -188,7 +220,7 @@ public class MetadataMojo extends AbstractMojo {
         int pcIndex = 0;
         for (ProvidedCapability capability : capabilities.getProvidedCapabilities()) {
             builder.append(CAPABILITY_NS).append(";");
-            builder.append(CAPABILITY_NS).append("=").append("\"").append(capability.getResourceType()).append("\"");
+            processListAttribute(CAPABILITY_RESOURCE_TYPE_AT, builder,capability.getResourceTypes());
             Optional.ofNullable(capability.getScriptEngine()).ifPresent(scriptEngine ->
                     builder.append(";")
                             .append(CAPABILITY_SCRIPT_ENGINE_AT).append("=").append("\"").append(scriptEngine).append("\"")
@@ -210,18 +242,8 @@ public class MetadataMojo extends AbstractMojo {
                             .append(CAPABILITY_EXTENSIONS_AT).append("=").append("\"").append(requestExtension).append("\"")
             );
             if (!capability.getSelectors().isEmpty()) {
-                builder.append(";").append(CAPABILITY_SELECTORS_AT).append("=").append("\"");
-                List<String> selectors = capability.getSelectors();
-                int selectorsSize = selectors.size();
-                int selectorIndex = 0;
-                for (String selector : selectors) {
-                    builder.append(selector);
-                    if (selectorIndex < selectorsSize - 1) {
-                        builder.append(",");
-                    }
-                    selectorIndex++;
-                }
-                builder.append("\"");
+                builder.append(";");
+                processListAttribute(CAPABILITY_SELECTORS_AT, builder, capability.getSelectors());
             }
             if (pcIndex < pcNum - 1) {
                 builder.append(",");
@@ -231,6 +253,20 @@ public class MetadataMojo extends AbstractMojo {
         return builder.toString();
     }
 
+    private void processListAttribute(@NotNull String capabilityAttribute, @NotNull StringBuilder builder, @NotNull Set<String> values) {
+        builder.append(capabilityAttribute).append("=").append("\"");
+        int valuesSize = values.size();
+        int valueIndex = 0;
+        for (String item : values) {
+            builder.append(item);
+            if (valueIndex < valuesSize - 1) {
+                builder.append(",");
+            }
+            valueIndex++;
+        }
+        builder.append("\"");
+    }
+
     @NotNull
     private String getRequiredCapabilitiesString(Capabilities capabilities) {
         StringBuilder builder = new StringBuilder();
diff --git a/src/main/java/org/apache/sling/scriptingbundle/maven/plugin/ProvidedCapability.java b/src/main/java/org/apache/sling/scriptingbundle/maven/plugin/ProvidedCapability.java
index c187418..c7afa70 100644
--- a/src/main/java/org/apache/sling/scriptingbundle/maven/plugin/ProvidedCapability.java
+++ b/src/main/java/org/apache/sling/scriptingbundle/maven/plugin/ProvidedCapability.java
@@ -19,26 +19,28 @@
 package org.apache.sling.scriptingbundle.maven.plugin;
 
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
 
 import org.apache.commons.lang3.StringUtils;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 class ProvidedCapability {
-    private final String resourceType;
+    private final Set<String> resourceTypes;
     private final String scriptEngine;
     private final String extendsResourceType;
     private final String version;
     private final String requestExtension;
     private final String requestMethod;
-    private final List<String> selectors;
+    private final Set<String> selectors;
 
-    private ProvidedCapability(@NotNull String resourceType, @Nullable String scriptEngine, @Nullable String extendsResourceType,
+    private ProvidedCapability(@NotNull Set<String> resourceTypes, @Nullable String scriptEngine, @Nullable String extendsResourceType,
                                @Nullable String version, @Nullable String requestExtension, @Nullable String requestMethod,
-                               @NotNull List<String> selectors) {
-        this.resourceType = resourceType;
+                               @NotNull Set<String> selectors) {
+        this.resourceTypes = resourceTypes;
         this.scriptEngine = scriptEngine;
         this.extendsResourceType = extendsResourceType;
         this.version = version;
@@ -52,8 +54,8 @@ class ProvidedCapability {
     }
 
     @NotNull
-    public String getResourceType() {
-        return resourceType;
+    public Set<String> getResourceTypes() {
+        return Collections.unmodifiableSet(resourceTypes);
     }
 
     @Nullable
@@ -82,13 +84,13 @@ class ProvidedCapability {
     }
 
     @NotNull
-    public List<String> getSelectors() {
-        return Collections.unmodifiableList(selectors);
+    public Set<String> getSelectors() {
+        return Collections.unmodifiableSet(selectors);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(resourceType, scriptEngine, version, requestExtension, extendsResourceType, requestMethod, selectors);
+        return Objects.hash(resourceTypes, scriptEngine, version, requestExtension, extendsResourceType, requestMethod, selectors);
     }
 
     @Override
@@ -98,7 +100,7 @@ class ProvidedCapability {
         }
         if (obj instanceof ProvidedCapability) {
             ProvidedCapability other = (ProvidedCapability) obj;
-            return Objects.equals(resourceType, other.resourceType) && Objects.equals(scriptEngine, other.scriptEngine) &&
+            return Objects.equals(resourceTypes, other.resourceTypes) && Objects.equals(scriptEngine, other.scriptEngine) &&
                     Objects.equals(version, other.version) && Objects.equals(requestExtension, other.requestExtension) &&
                     Objects.equals(extendsResourceType, other.extendsResourceType) && Objects.equals(requestMethod, other.requestMethod) &&
                     Objects.equals(selectors, other.selectors);
@@ -109,25 +111,35 @@ class ProvidedCapability {
     @Override
     public String toString() {
         return String.format(
-            "%s{resourceType=%s, scriptEngine=%s, version=%s, selectors=%s, requestExtension=%s, requestMethod=%s, extendsResourceType=%s}",
-            this.getClass().getSimpleName(), resourceType, scriptEngine, version, selectors, requestExtension, requestMethod, extendsResourceType
+            "%s{resourceTypes=%s, scriptEngine=%s, version=%s, selectors=%s, requestExtension=%s, requestMethod=%s, " +
+                    "extendsResourceType=%s}",
+            this.getClass().getSimpleName(), resourceTypes, scriptEngine, version, selectors, requestExtension, requestMethod,
+                extendsResourceType
         );
     }
 
     static class Builder {
-        private String resourceType;
+        private Set<String> resourceTypes = new HashSet<>();
         private String scriptEngine;
         private String extendsResourceType;
         private String version;
         private String requestExtension;
         private String requestMethod;
-        private List<String> selectors = Collections.emptyList();
+        private Set<String> selectors = Collections.emptySet();
+
+        Builder withResourceTypes(Set<String> resourceTypes) {
+            if (resourceTypes == null || resourceTypes.isEmpty()) {
+                throw new NullPointerException("The script's resourceTypes cannot be null or empty.");
+            }
+            this.resourceTypes = resourceTypes;
+            return this;
+        }
 
         Builder withResourceType(String resourceType) {
             if (StringUtils.isEmpty(resourceType)) {
                 throw new NullPointerException("The script's resourceType cannot be null or empty.");
             }
-            this.resourceType = resourceType;
+            resourceTypes.add(resourceType);
             return this;
         }
 
@@ -156,7 +168,7 @@ class ProvidedCapability {
             return this;
         }
 
-        Builder withSelectors(List<String> selectors) {
+        Builder withSelectors(Set<String> selectors) {
             if (selectors == null) {
                 throw new NullPointerException("The resourceType selectors list cannot be null.");
             }
@@ -165,7 +177,10 @@ class ProvidedCapability {
         }
 
         ProvidedCapability build() {
-            return new ProvidedCapability(resourceType, scriptEngine, extendsResourceType, version, requestExtension, requestMethod,
+            if (resourceTypes.isEmpty()) {
+                throw new IllegalStateException("The resourceTypes set is empty.");
+            }
+            return new ProvidedCapability(resourceTypes, scriptEngine, extendsResourceType, version, requestExtension, requestMethod,
                     selectors);
         }
     }
diff --git a/src/main/java/org/apache/sling/scriptingbundle/maven/plugin/ResourceTypeFolderAnalyser.java b/src/main/java/org/apache/sling/scriptingbundle/maven/plugin/ResourceTypeFolderAnalyser.java
index b43a218..64aebdb 100644
--- a/src/main/java/org/apache/sling/scriptingbundle/maven/plugin/ResourceTypeFolderAnalyser.java
+++ b/src/main/java/org/apache/sling/scriptingbundle/maven/plugin/ResourceTypeFolderAnalyser.java
@@ -24,6 +24,7 @@ import java.nio.file.DirectoryStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
@@ -32,9 +33,9 @@ import java.util.stream.Stream;
 
 import org.apache.commons.lang3.StringUtils;
 import org.apache.maven.plugin.logging.Log;
+import org.apache.sling.scripting.bundle.tracker.ResourceType;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
-import org.osgi.framework.Version;
 import org.osgi.framework.VersionRange;
 
 
@@ -43,75 +44,57 @@ class ResourceTypeFolderAnalyser {
     private final Log log;
     private final Path scriptsDirectory;
     private final ResourceTypeFolderPredicate resourceTypeFolderPredicate;
+    private final Map<String, String> scriptEngineMappings;
+    private final Set<String> searchPaths;
 
-    ResourceTypeFolderAnalyser(@NotNull Log log, @NotNull Path scriptsDirectory) {
+    ResourceTypeFolderAnalyser(@NotNull Log log, @NotNull Path scriptsDirectory, @NotNull Map<String, String> scriptEngineMappings,
+                               @NotNull Set<String> searchPaths) {
         this.log = log;
         this.scriptsDirectory = scriptsDirectory;
         this.resourceTypeFolderPredicate = new ResourceTypeFolderPredicate(log);
+        this.scriptEngineMappings = scriptEngineMappings;
+        this.searchPaths = searchPaths;
     }
 
-    Capabilities getCapabilities(@NotNull Path resourceTypeDirectory, @NotNull Map<String, String> scriptEngineMappings) {
+    Capabilities getCapabilities(@NotNull Path resourceTypeDirectory) {
         Set<ProvidedCapability> providedCapabilities = new LinkedHashSet<>();
         Set<RequiredCapability> requiredCapabilities = new LinkedHashSet<>();
-        String version = null;
-        String resourceType = null;
-        try {
-            Path fileName = resourceTypeDirectory.getFileName();
-            if (fileName != null) {
-                version = Version.parseVersion(fileName.toString()).toString();
-            }
-        } catch (IllegalArgumentException ignored) {
-            log.debug("No resourceType version detected in " + scriptsDirectory.resolve(resourceTypeDirectory).toString());
-        }
-        if (StringUtils.isNotEmpty(version)) {
-            Path parent = resourceTypeDirectory.getParent();
-            if (parent != null) {
-                resourceType = getResourceType(parent);
-            }
-        } else {
-            resourceType = getResourceType(resourceTypeDirectory);
-        }
-        if (resourceType != null) {
-            String resourceTypeLabel = getResourceTypeLabel(resourceType);
-            String finalResourceType = resourceType;
-            String finalVersion = version;
-            try (DirectoryStream<Path> resourceTypeDirectoryStream = Files.newDirectoryStream(scriptsDirectory.resolve(resourceTypeDirectory))) {
-                resourceTypeDirectoryStream.forEach(entry -> {
-                    if (Files.isRegularFile(entry)) {
-                        Path file = entry.getFileName();
-                        if (file != null) {
-                            if (MetadataMojo.EXTENDS_FILE.equals(file.toString())) {
-                                processExtendsFile(finalResourceType, finalVersion, entry, providedCapabilities, requiredCapabilities);
-                            } else if (MetadataMojo.REQUIRES_FILE.equals(file.toString())) {
-                                processRequiresFile(entry, requiredCapabilities);
-                            } else {
-                                processScriptFile(resourceTypeDirectory, scriptEngineMappings, entry, finalResourceType, finalVersion,
-                                        resourceTypeLabel, providedCapabilities);
-                            }
+        try (DirectoryStream<Path> resourceTypeDirectoryStream = Files.newDirectoryStream(scriptsDirectory.resolve(resourceTypeDirectory))) {
+            Path relativeResourceTypeDirectory = scriptsDirectory.relativize(resourceTypeDirectory);
+            final ResourceType resourceType = ResourceType.parseResourceType(relativeResourceTypeDirectory.toString());
+            resourceTypeDirectoryStream.forEach(entry -> {
+                if (Files.isRegularFile(entry)) {
+                    Path file = entry.getFileName();
+                    if (file != null) {
+                        if (MetadataMojo.EXTENDS_FILE.equals(file.toString())) {
+                            processExtendsFile(resourceType.getType(), resourceType.getVersion(), entry, providedCapabilities, requiredCapabilities);
+                        } else if (MetadataMojo.REQUIRES_FILE.equals(file.toString())) {
+                            processRequiresFile(entry, requiredCapabilities);
+                        } else {
+                            processScriptFile(resourceTypeDirectory, entry, resourceType, providedCapabilities);
                         }
-                    } else if (Files.isDirectory(entry) && !resourceTypeFolderPredicate.test(entry)) {
-                        try (Stream<Path> selectorFilesStream = Files.walk(entry).filter(Files::isRegularFile).filter(file -> {
-                            Path fileParent = file.getParent();
-                            while (!resourceTypeDirectory.equals(fileParent)) {
-                                if (resourceTypeFolderPredicate.test(fileParent)) {
-                                    return false;
-                                }
-                                fileParent = fileParent.getParent();
+                    }
+                } else if (Files.isDirectory(entry) && !resourceTypeFolderPredicate.test(entry)) {
+                    try (Stream<Path> selectorFilesStream = Files.walk(entry).filter(Files::isRegularFile).filter(file -> {
+                        Path fileParent = file.getParent();
+                        while (!resourceTypeDirectory.equals(fileParent)) {
+                            if (resourceTypeFolderPredicate.test(fileParent)) {
+                                return false;
                             }
-                            return true;
-                        })) {
-                            selectorFilesStream.forEach(
-                                    file -> processScriptFile(resourceTypeDirectory, scriptEngineMappings, file, finalResourceType,
-                                            finalVersion, resourceTypeLabel, providedCapabilities)
-                            );
-                        } catch (IOException e) {
-                            log.error(String.format("Unable to scan folder %s.", entry.toString()), e);
+                            fileParent = fileParent.getParent();
                         }
+                        return true;
+                    })) {
+                        selectorFilesStream.forEach(
+                                file -> processScriptFile(resourceTypeDirectory, file, resourceType, providedCapabilities)
+                        );
+                    } catch (IOException e) {
+                        log.error(String.format("Unable to scan folder %s.", entry.toString()), e);
                     }
-                });
-            } catch (IOException | IllegalArgumentException e) {
-                log.warn(String.format("Cannot analyse folder %s.", scriptsDirectory.resolve(resourceTypeDirectory).toString()), e);
-            }
+                }
+            });
+        } catch (IOException | IllegalArgumentException e) {
+            log.warn(String.format("Cannot analyse folder %s.", scriptsDirectory.resolve(resourceTypeDirectory).toString()), e);
         }
 
         return new Capabilities(providedCapabilities, requiredCapabilities);
@@ -175,14 +158,13 @@ class ResourceTypeFolderAnalyser {
         }
     }
 
-    private void processScriptFile(@NotNull Path resourceTypeDirectory, @NotNull Map<String, String> scriptEngineMappings,
-                                   @NotNull Path scriptPath, @NotNull String resourceType, @Nullable String version,
-                                   @NotNull String resourceTypeLabel, @NotNull Set<ProvidedCapability> providedCapabilities) {
+    private void processScriptFile(@NotNull Path resourceTypeDirectory, @NotNull Path scriptPath,
+                                   @NotNull ResourceType resourceType, @NotNull Set<ProvidedCapability> providedCapabilities) {
         Path scriptFile = scriptPath.getFileName();
         if (scriptFile != null) {
             Path relativeResourceTypeFolder = resourceTypeDirectory.relativize(scriptPath);
             int pathSegments = relativeResourceTypeFolder.getNameCount();
-            ArrayList<String> selectors = new ArrayList<>();
+            LinkedHashSet<String> selectors = new LinkedHashSet<>();
             if (pathSegments > 1) {
                 for (int i = 0; i < pathSegments - 1; i++) {
                     selectors.add(relativeResourceTypeFolder.getName(i).toString());
@@ -194,7 +176,21 @@ class ResourceTypeFolderAnalyser {
                 String scriptEngine = scriptEngineMappings.get(script.getScriptExtension());
                 if (scriptEngine != null) {
                     String scriptName = script.getName();
-                    if (!resourceTypeLabel.equals(scriptName)) {
+                    Set<String> resourceTypes = new HashSet<>();
+                    for (String searchPath : searchPaths) {
+                        if (!searchPath.endsWith("/")) {
+                            searchPath = searchPath + "/";
+                        }
+                        String absoluteType = "/" + resourceType.getType();
+                        if (absoluteType.startsWith(searchPath)) {
+                            resourceTypes.add(absoluteType);
+                            resourceTypes.add(absoluteType.substring(searchPath.length()));
+                        }
+                    }
+                    if (resourceTypes.isEmpty()) {
+                        resourceTypes.add(resourceType.getType());
+                    }
+                    if (!resourceType.getResourceLabel().equals(scriptName)) {
                         if (scriptFileName.split("\\.").length == 2 && scriptName != null &&
                                 scriptName.equals(script.getRequestExtension())) {
                             /*
@@ -203,12 +199,12 @@ class ResourceTypeFolderAnalyser {
                                 1. capability for the name as a selector
                                 2. capability for the name as a request extension
                              */
-                            ArrayList<String> capSelectors = new ArrayList<>(selectors);
+                            LinkedHashSet<String> capSelectors = new LinkedHashSet<>(selectors);
                             capSelectors.add(scriptName);
                             providedCapabilities.add(
                                     ProvidedCapability.builder()
-                                            .withResourceType(resourceType)
-                                            .withVersion(version)
+                                            .withResourceTypes(resourceTypes)
+                                            .withVersion(resourceType.getVersion())
                                             .withSelectors(capSelectors)
                                             .withRequestMethod(script.getRequestMethod())
                                             .withScriptEngine(scriptEngine)
@@ -216,8 +212,8 @@ class ResourceTypeFolderAnalyser {
                             );
                             providedCapabilities.add(
                                     ProvidedCapability.builder()
-                                            .withResourceType(resourceType)
-                                            .withVersion(version)
+                                            .withResourceTypes(resourceTypes)
+                                            .withVersion(resourceType.getVersion())
                                             .withSelectors(selectors)
                                             .withRequestExtension(script.getRequestExtension())
                                             .withRequestMethod(script.getRequestMethod())
@@ -232,8 +228,8 @@ class ResourceTypeFolderAnalyser {
                     }
                     providedCapabilities.add(
                             ProvidedCapability.builder()
-                                    .withResourceType(resourceType)
-                                    .withVersion(version)
+                                    .withResourceTypes(resourceTypes)
+                                    .withVersion(resourceType.getVersion())
                                     .withSelectors(selectors)
                                     .withRequestExtension(script.getRequestExtension())
                                     .withRequestMethod(script.getRequestMethod())
@@ -250,40 +246,4 @@ class ResourceTypeFolderAnalyser {
             log.warn(String.format("Skipping path %s since it has 0 elements.", scriptPath.toString()));
         }
     }
-
-    private String getResourceType(@NotNull Path resourceTypeFolder) {
-        StringBuilder stringBuilder = new StringBuilder();
-        Path relativeResourceTypePath = scriptsDirectory.relativize(resourceTypeFolder);
-        if (StringUtils.isNotEmpty(relativeResourceTypePath.toString())) {
-            int parts = relativeResourceTypePath.getNameCount();
-            for (int i = 0; i < parts; i++) {
-                stringBuilder.append(relativeResourceTypePath.getName(i));
-                if (i < parts - 1) {
-                    stringBuilder.append('/');
-                }
-            }
-        }
-        return stringBuilder.toString();
-    }
-
-    private @NotNull String getResourceTypeLabel(@NotNull String resourceType) {
-        String resourceTypeLabel = null;
-        if (resourceType.contains("/")) {
-            int lastIndex = resourceType.lastIndexOf('/');
-            if (lastIndex < resourceType.length() - 2) {
-                resourceTypeLabel = resourceType.substring(++lastIndex);
-            }
-        } else if (resourceType.contains(".")) {
-            int lastIndex = resourceType.lastIndexOf('.');
-            if (lastIndex < resourceType.length() - 2) {
-                resourceTypeLabel = resourceType.substring(++lastIndex);
-            }
-        } else {
-            resourceTypeLabel = resourceType;
-        }
-        if (StringUtils.isEmpty(resourceTypeLabel)) {
-            throw new IllegalArgumentException(String.format("Resource type '%s' does not provide a resourceTypeLabel.", resourceType));
-        }
-        return resourceTypeLabel;
-    }
 }
diff --git a/src/test/java/org/apache/sling/scriptingbundle/maven/plugin/MetadataMojoTest.java b/src/test/java/org/apache/sling/scriptingbundle/maven/plugin/MetadataMojoTest.java
index 7586ece..44e848f 100644
--- a/src/test/java/org/apache/sling/scriptingbundle/maven/plugin/MetadataMojoTest.java
+++ b/src/test/java/org/apache/sling/scriptingbundle/maven/plugin/MetadataMojoTest.java
@@ -21,7 +21,9 @@ package org.apache.sling.scriptingbundle.maven.plugin;
 import java.io.File;
 import java.nio.file.Paths;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.Set;
 
 import org.apache.maven.execution.MavenSession;
@@ -51,57 +53,80 @@ public class MetadataMojoTest {
         MojoProject mojoProject = getMojoProject(getProjectLocation("project-1"));
         mojoProject.mojo.execute();
         Capabilities capabilities = mojoProject.mojo.getCapabilities();
-
         Set<ProvidedCapability> pExpected = new HashSet<>(Arrays.asList(
                 // org/apache/sling/bar/1.0.0
                 ProvidedCapability.builder().withResourceType("org/apache/sling/bar").withScriptEngine("htl").withVersion("1.0.0").build(),
-                ProvidedCapability.builder().withResourceType("org/apache/sling/bar").withScriptEngine("htl").withVersion("1.0.0").withSelectors(Arrays.asList("depth1"
-                        , "100")).build(),
-                ProvidedCapability.builder().withResourceType("org/apache/sling/bar").withScriptEngine("htl").withVersion("1.0.0").withSelectors(Arrays.asList("depth1"
-                        , "200")).build(),
-                ProvidedCapability.builder().withResourceType("org/apache/sling/bar").withScriptEngine("htl").withVersion("1.0.0").withSelectors(Arrays.asList("depth1"
-                        , "depth2", "100")).build(),
+                ProvidedCapability.builder().withResourceType("org/apache/sling/bar").withScriptEngine("htl").withVersion("1.0.0").withSelectors(new LinkedHashSet<>(Arrays.asList("depth1"
+                        , "100"))).build(),
+                ProvidedCapability.builder().withResourceType("org/apache/sling/bar").withScriptEngine("htl").withVersion("1.0.0").withSelectors(new LinkedHashSet<>(Arrays.asList("depth1"
+                        , "200"))).build(),
+                ProvidedCapability.builder().withResourceType("org/apache/sling/bar").withScriptEngine("htl").withVersion("1.0.0").withSelectors(new LinkedHashSet<>(Arrays.asList("depth1"
+                        , "depth2", "100"))).build(),
 
                 // org/apache/sling/foo
                 ProvidedCapability.builder().withResourceType("org/apache/sling/foo").withScriptEngine("htl").build(),
-                ProvidedCapability.builder().withResourceType("org/apache/sling/foo").withScriptEngine("htl").withSelectors(Arrays.asList("depth1"
-                        , "100")).build(),
-                ProvidedCapability.builder().withResourceType("org/apache/sling/foo").withScriptEngine("htl").withSelectors(Arrays.asList("depth1"
-                        , "200")).build(),
-                ProvidedCapability.builder().withResourceType("org/apache/sling/foo").withScriptEngine("htl").withSelectors(Arrays.asList("depth1"
-                        , "depth2", "100")).build(),
+                ProvidedCapability.builder().withResourceType("org/apache/sling/foo").withScriptEngine("htl").withSelectors(new LinkedHashSet<>(Arrays.asList("depth1"
+                        , "100"))).build(),
+                ProvidedCapability.builder().withResourceType("org/apache/sling/foo").withScriptEngine("htl").withSelectors(new LinkedHashSet<>(Arrays.asList("depth1"
+                        , "200"))).build(),
+                ProvidedCapability.builder().withResourceType("org/apache/sling/foo").withScriptEngine("htl").withSelectors(new LinkedHashSet<>(Arrays.asList("depth1"
+                        , "depth2", "100"))).build(),
 
                 // org/apache/sling/foo/depth1/depth2/depth3
                 ProvidedCapability.builder().withResourceType("org/apache/sling/foo/depth1/depth2/depth3").withExtendsResourceType("org" +
                         "/apache/sling/bar").build(),
-                ProvidedCapability.builder().withResourceType("org/apache/sling/foo/depth1/depth2/depth3").withScriptEngine("htl").withSelectors(Arrays.asList("depth3-selector")).build(),
+                ProvidedCapability.builder().withResourceType("org/apache/sling/foo/depth1/depth2/depth3").withScriptEngine("htl").withSelectors(new LinkedHashSet<>(Arrays.asList("depth3-selector"))).build(),
 
                 // org.apache.sling.foobar/1.0.0
                 ProvidedCapability.builder().withResourceType("org.apache.sling.foobar").withScriptEngine("htl").withVersion("1.0.0").build(),
                 ProvidedCapability.builder().withResourceType("org.apache.sling.foobar").withVersion("1.0.0").withExtendsResourceType("org/apache/sling/bar").build(),
-                ProvidedCapability.builder().withResourceType("org.apache.sling.foobar").withScriptEngine("htl").withVersion("1.0.0").withSelectors(Arrays.asList("depth1"
-                        , "100")).build(),
-                ProvidedCapability.builder().withResourceType("org.apache.sling.foobar").withScriptEngine("htl").withVersion("1.0.0").withSelectors(Arrays.asList("depth1"
-                        , "200")).build(),
-                ProvidedCapability.builder().withResourceType("org.apache.sling.foobar").withScriptEngine("htl").withVersion("1.0.0").withSelectors(Arrays.asList("depth1"
-                        , "depth2", "100")).build(),
+                ProvidedCapability.builder().withResourceType("org.apache.sling.foobar").withScriptEngine("htl").withVersion("1.0.0").withSelectors(new LinkedHashSet<>(Arrays.asList("depth1"
+                        , "100"))).build(),
+                ProvidedCapability.builder().withResourceType("org.apache.sling.foobar").withScriptEngine("htl").withVersion("1.0.0").withSelectors(new LinkedHashSet<>(Arrays.asList("depth1"
+                        , "200"))).build(),
+                ProvidedCapability.builder().withResourceType("org.apache.sling.foobar").withScriptEngine("htl").withVersion("1.0.0").withSelectors(new LinkedHashSet<>(Arrays.asList("depth1"
+                        , "depth2", "100"))).build(),
 
                 // org.apache.sling.foobar
                 ProvidedCapability.builder().withResourceType("org.apache.sling.foobar").withExtendsResourceType("org/apache/sling/bar").build(),
-                ProvidedCapability.builder().withResourceType("org.apache.sling.foobar").withScriptEngine("htl").withSelectors(Arrays.asList("depth1"
-                        , "100")).build(),
-                ProvidedCapability.builder().withResourceType("org.apache.sling.foobar").withScriptEngine("htl").withSelectors(Arrays.asList("depth1"
-                        , "200")).build(),
-                ProvidedCapability.builder().withResourceType("org.apache.sling.foobar").withScriptEngine("htl").withSelectors(Arrays.asList("depth1"
-                        , "depth2", "100")).build(),
+                ProvidedCapability.builder().withResourceType("org.apache.sling.foobar").withScriptEngine("htl").withSelectors(new LinkedHashSet<>(Arrays.asList("depth1"
+                        , "100"))).build(),
+                ProvidedCapability.builder().withResourceType("org.apache.sling.foobar").withScriptEngine("htl").withSelectors(new LinkedHashSet<>(Arrays.asList("depth1"
+                        , "200"))).build(),
+                ProvidedCapability.builder().withResourceType("org.apache.sling.foobar").withScriptEngine("htl").withSelectors(new LinkedHashSet<>(Arrays.asList("depth1"
+                        , "depth2", "100"))).build(),
                 ProvidedCapability.builder().withResourceType("org.apache.sling.foobar").withScriptEngine("htl").withRequestMethod("GET").build(),
-                ProvidedCapability.builder().withResourceType("org.apache.sling.foobar").withScriptEngine("htl").withRequestMethod("GET").withSelectors(Arrays.asList("test")).build(),
-                ProvidedCapability.builder().withResourceType("org.apache.sling.foobar").withScriptEngine("htl").withRequestMethod("GET").withSelectors(Arrays.asList("test")).withRequestExtension("txt").build(),
-                ProvidedCapability.builder().withResourceType("org.apache.sling.foobar").withScriptEngine("htl").withSelectors(Arrays.asList("test")).withRequestExtension("txt").build(),
+                ProvidedCapability.builder().withResourceType("org.apache.sling.foobar").withScriptEngine("htl").withRequestMethod("GET").withSelectors(new LinkedHashSet<>(Arrays.asList("test"))).build(),
+                ProvidedCapability.builder().withResourceType("org.apache.sling.foobar").withScriptEngine("htl").withRequestMethod("GET").withSelectors(new LinkedHashSet<>(Arrays.asList("test"))).withRequestExtension("txt").build(),
+                ProvidedCapability.builder().withResourceType("org.apache.sling.foobar").withScriptEngine("htl").withSelectors(new LinkedHashSet<>(Arrays.asList("test"))).withRequestExtension("txt").build(),
 
                 // sling
-                ProvidedCapability.builder().withResourceType("sling").withScriptEngine("htl").build()
+                ProvidedCapability.builder().withResourceType("sling").withScriptEngine("htl").build(),
+
+                // sling/test
+                ProvidedCapability.builder().withResourceType("/libs/sling/test").withResourceType("sling/test").withScriptEngine("htl").build()
+        ));
+
+        Set<RequiredCapability> rExpected = new HashSet<>(Arrays.asList(
+                RequiredCapability.builder().withResourceType("sling/default").withVersionRange(VersionRange.valueOf("[1.0.0,2.0.0)")).build(),
+                RequiredCapability.builder().withResourceType("org/apache/sling/bar").build(),
+                RequiredCapability.builder().withResourceType("org/apache/sling/bar").withVersionRange(VersionRange.valueOf("[1.0.0,2.0.0)")).build()
+        ));
+        verifyCapabilities(capabilities, pExpected, rExpected);
+    }
+
+    @Test
+    public void testProject2() throws Exception {
+        MojoProject mojoProject = getMojoProject(getProjectLocation("project-2"));
+        mojoProject.mojo.execute();
+        Capabilities capabilities = mojoProject.mojo.getCapabilities();
+        Set<ProvidedCapability> pExpected = new HashSet<>(Arrays.asList(
+                ProvidedCapability.builder().withResourceType("libs/sling/test").withScriptEngine("thymeleaf").build()
         ));
+        verifyCapabilities(capabilities, pExpected, Collections.emptySet());
+    }
+
+    private void verifyCapabilities(Capabilities capabilities, Set<ProvidedCapability> pExpected, Set<RequiredCapability> rExpected) {
         Set<ProvidedCapability> provided = new HashSet<>(capabilities.getProvidedCapabilities());
         StringBuilder missingProvided = new StringBuilder();
         for (ProvidedCapability capability : pExpected) {
@@ -121,11 +146,6 @@ public class MetadataMojoTest {
             fail(extraProvided.toString());
         }
 
-        Set<RequiredCapability> rExpected = new HashSet<>(Arrays.asList(
-                RequiredCapability.builder().withResourceType("sling/default").withVersionRange(VersionRange.valueOf("[1.0.0,2.0.0)")).build(),
-                RequiredCapability.builder().withResourceType("org/apache/sling/bar").build(),
-                RequiredCapability.builder().withResourceType("org/apache/sling/bar").withVersionRange(VersionRange.valueOf("[1.0.0,2.0.0)")).build()
-        ));
         Set<RequiredCapability> required = new HashSet<>(capabilities.getRequiredCapabilities());
         assertEquals(rExpected.size(), required.size());
         StringBuilder missingRequired = new StringBuilder();
@@ -138,6 +158,13 @@ public class MetadataMojoTest {
         if (missingRequired.length() > 0) {
             fail(missingRequired.toString());
         }
+        StringBuilder extraRequired = new StringBuilder();
+        for (RequiredCapability capability : required) {
+            extraRequired.append("Extra required capability: ").append(capability.toString()).append(System.lineSeparator());
+        }
+        if (extraRequired.length() > 0) {
+            fail(extraRequired.toString());
+        }
     }
 
     private MojoProject getMojoProject(File projectDirectory) throws Exception {
diff --git a/src/test/resources/project-1/src/main/scripts/libs/sling/test/test.html b/src/test/resources/project-1/src/main/scripts/libs/sling/test/test.html
new file mode 100644
index 0000000..2853663
--- /dev/null
+++ b/src/test/resources/project-1/src/main/scripts/libs/sling/test/test.html
@@ -0,0 +1,18 @@
+<!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements.  See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership.  The ASF licenses this file
+  ~ to you under the Apache License, Version 2.0 (the
+  ~ "License"); you may not use this file except in compliance
+  ~ with the License.  You may obtain a copy of the License at
+  ~
+  ~   http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing,
+  ~ software distributed under the License is distributed on an
+  ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  ~ KIND, either express or implied.  See the License for the
+  ~ specific language governing permissions and limitations
+  ~ under the License.
+  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-->
diff --git a/src/test/resources/project-2/pom.xml b/src/test/resources/project-2/pom.xml
new file mode 100644
index 0000000..0f089bb
--- /dev/null
+++ b/src/test/resources/project-2/pom.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements.  See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership.  The ASF licenses this file
+  ~ to you under the Apache License, Version 2.0 (the
+  ~ "License"); you may not use this file except in compliance
+  ~ with the License.  You may obtain a copy of the License at
+  ~
+  ~   http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing,
+  ~ software distributed under the License is distributed on an
+  ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  ~ KIND, either express or implied.  See the License for the
+  ~ specific language governing permissions and limitations
+  ~ under the License.
+  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>org.apache.sling</groupId>
+    <artifactId>scriptingbundle-maven-plugin-project-1</artifactId>
+    <version>0.0.1</version>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.sling</groupId>
+                <artifactId>scriptingbundle-maven-plugin</artifactId>
+                <configuration>
+                    <searchPaths>
+                        <searchPath>/bin</searchPath>
+                    </searchPaths>
+                    <scriptEngineMappings>
+                        <html>thymeleaf</html>
+                    </scriptEngineMappings>
+                </configuration>
+                <executions>
+                    <execution>
+                        <phase>prepare-package</phase>
+                        <goals>
+                            <goal>metadata</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/src/test/resources/project-2/src/main/scripts/libs/sling/test/test.html b/src/test/resources/project-2/src/main/scripts/libs/sling/test/test.html
new file mode 100644
index 0000000..2853663
--- /dev/null
+++ b/src/test/resources/project-2/src/main/scripts/libs/sling/test/test.html
@@ -0,0 +1,18 @@
+<!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements.  See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership.  The ASF licenses this file
+  ~ to you under the Apache License, Version 2.0 (the
+  ~ "License"); you may not use this file except in compliance
+  ~ with the License.  You may obtain a copy of the License at
+  ~
+  ~   http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing,
+  ~ software distributed under the License is distributed on an
+  ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  ~ KIND, either express or implied.  See the License for the
+  ~ specific language governing permissions and limitations
+  ~ under the License.
+  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-->