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 2021/08/05 14:59:32 UTC

[sling-org-apache-sling-graphql-schema-aggregator] branch master updated: SLING-10680 - Add support for versioning partials

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-org-apache-sling-graphql-schema-aggregator.git


The following commit(s) were added to refs/heads/master by this push:
     new a3c54a4  SLING-10680 - Add support for versioning partials
a3c54a4 is described below

commit a3c54a4f98dac0266360ce5b860ca96c75516af0
Author: Radu Cotescu <17...@users.noreply.github.com>
AuthorDate: Thu Aug 5 16:59:28 2021 +0200

    SLING-10680 - Add support for versioning partials
    
    * partials can optionally provide a version, which can now also be used for REQUIRES
---
 pom.xml                                            |  6 ++++
 .../schema/aggregator/impl/BundleEntryPartial.java |  2 +-
 .../aggregator/impl/DefaultSchemaAggregator.java   | 36 +++++++++++---------
 .../graphql/schema/aggregator/impl/Partial.java    | 30 ++++++++++++++---
 .../schema/aggregator/impl/PartialInfo.java        | 37 +++++++++++++++++++-
 .../schema/aggregator/impl/PartialReader.java      | 39 ++++++++++++----------
 .../aggregator/impl/ProviderBundleTracker.java     | 10 +++---
 .../impl/DefaultSchemaAggregatorTest.java          | 38 +++++++++++++++------
 .../schema/aggregator/impl/PartialReaderTest.java  |  9 +++++
 .../aggregator/impl/ProviderBundleTrackerTest.java |  2 +-
 .../aggregator/it/SchemaAggregatorTestSupport.java |  3 +-
 src/test/resources/partials/required-1.0.0.txt     |  4 +++
 src/test/resources/partials/versioned-1.0.0.txt    |  3 ++
 src/test/resources/partials/versioned-2.0.0.txt    |  3 ++
 .../partials/versionedPartials-output.txt          | 10 ++++++
 15 files changed, 176 insertions(+), 56 deletions(-)

diff --git a/pom.xml b/pom.xml
index cd56bb3..e216214 100644
--- a/pom.xml
+++ b/pom.xml
@@ -175,6 +175,12 @@
       <scope>provided</scope>
     </dependency>
     <dependency>
+      <groupId>commons-codec</groupId>
+      <artifactId>commons-codec</artifactId>
+      <version>1.15</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
       <groupId>org.slf4j</groupId>
       <artifactId>slf4j-api</artifactId>
       <scope>provided</scope>
diff --git a/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/BundleEntryPartial.java b/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/BundleEntryPartial.java
index 3207e0a..a033db5 100644
--- a/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/BundleEntryPartial.java
+++ b/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/BundleEntryPartial.java
@@ -74,6 +74,6 @@ class BundleEntryPartial extends PartialReader implements Comparable<BundleEntry
 
     @Override
     public int compareTo(BundleEntryPartial o) {
-        return getName().compareTo(o.getName());
+        return getPartialInfo().compareTo(o.getPartialInfo());
     }
 }
diff --git a/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/DefaultSchemaAggregator.java b/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/DefaultSchemaAggregator.java
index 6f8a8dd..c3e58c4 100644
--- a/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/DefaultSchemaAggregator.java
+++ b/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/DefaultSchemaAggregator.java
@@ -49,7 +49,7 @@ public class DefaultSchemaAggregator implements SchemaAggregator {
         NO_BLOCK,
         WITH_BLOCK_IF_NOT_EMPTY,
         WITH_BLOCK
-    };
+    }
 
     @Reference
     private ProviderBundleTracker tracker;
@@ -91,7 +91,7 @@ public class DefaultSchemaAggregator implements SchemaAggregator {
     }
 
     private void writeSourceInfo(Writer target, Partial p) throws IOException {
-        target.write(String.format("%n# %s.source=%s%n", getClass().getSimpleName(), p.getName()));
+        target.write(String.format("%n# %s.source=%s%n", getClass().getSimpleName(), p.getPartialInfo()));
     }
 
     @Override
@@ -100,7 +100,7 @@ public class DefaultSchemaAggregator implements SchemaAggregator {
         target.write(String.format("# %s", info));
 
         // build list of selected providers
-        final Map<String, Partial> providers = tracker.getSchemaProviders();
+        final Map<PartialInfo, Partial> providers = tracker.getSchemaProviders();
         if(log.isDebugEnabled()) {
             log.debug("Aggregating schemas, request={}, providers={}", Arrays.asList(providerNamesOrRegexp), providers.keySet());
         }
@@ -123,51 +123,57 @@ public class DefaultSchemaAggregator implements SchemaAggregator {
             if(partialNames.length() > 0) {
                 partialNames.append(",");
             }
-            partialNames.append(p.getName());
+            partialNames.append(p.getPartialInfo());
         });
         target.write(String.format("%n# End of Schema aggregated from {%s} by %s", partialNames, getClass().getSimpleName()));
     }
 
-    Set<Partial> selectProviders(Map<String, Partial> providers, Set<String> missing, String ... providerNamesOrRegexp) {
+    Set<Partial> selectProviders(Map<PartialInfo, Partial> providers, Set<String> missing, String ... providerNamesOrRegexp) {
         final Set<Partial> result= new LinkedHashSet<>();
         for(String str : providerNamesOrRegexp) {
             final Pattern p = toRegexp(str);
             if(p != null) {
                 log.debug("Selecting providers matching {}", p);
                 providers.entrySet().stream()
-                    .filter(e -> p.matcher(e.getKey()).matches())
-                    .sorted(Comparator.comparing(e -> e.getValue().getName()))
+                    .filter(e -> p.matcher(e.getKey().getName()).matches())
+                    .sorted(Comparator.comparing(e -> e.getValue().getPartialInfo()))
                     .forEach(e -> addWithRequirements(providers, result, missing, e.getValue(), 0))
                 ;
             } else {
                 log.debug("Selecting provider with key={}", str);
-                final Partial psp = providers.get(str);
-                if(psp == null) {
+                Optional<PartialInfo> fromString = PartialInfo.fromRequiresSection(str).stream().findFirst();
+                if (fromString.isPresent()) {
+                    PartialInfo selected = fromString.get();
+                    final Partial psp = providers.get(selected);
+                    if (psp == null) {
+                        missing.add(str);
+                        continue;
+                    }
+                    addWithRequirements(providers, result, missing, psp, 0);
+                } else {
                     missing.add(str);
-                    continue;
                 }
-                addWithRequirements(providers, result, missing, psp, 0);
             }
         }
         return result;
     }
 
-    private void addWithRequirements(Map<String, Partial> providers, Set<Partial> addTo, Set<String> missing, Partial p, int recursionLevel) {
+    private void addWithRequirements(Map<PartialInfo, Partial> providers, Set<Partial> addTo, Set<String> missing, Partial p, int recursionLevel) {
 
         // simplistic cycle detection
         if(recursionLevel > MAX_REQUIREMENTS_RECURSION_LEVEL) {
             throw new RuntimeException(String.format(
                 "Requirements depth over %d, requirements cycle suspected at partial %s", 
                 MAX_REQUIREMENTS_RECURSION_LEVEL,
-                p.getName()
+                p.getPartialInfo()
             ));
         }
 
         addTo.add(p);
-        for(String req : p.getRequiredPartialNames()) {
+        for(PartialInfo req : p.getRequiredPartialNames()) {
             final Partial preq = providers.get(req);
             if(preq == null) {
-                missing.add(req);
+                missing.add(req.toString());
             } else {
                 addWithRequirements(providers, addTo, missing, preq, recursionLevel + 1);
             }
diff --git a/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/Partial.java b/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/Partial.java
index 9e14d35..f0268a9 100644
--- a/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/Partial.java
+++ b/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/Partial.java
@@ -24,7 +24,6 @@ import java.util.Optional;
 import java.util.Set;
 
 import org.jetbrains.annotations.NotNull;
-import org.osgi.framework.Version;
 
 /** Wrapper for the partials format, that parses a partial file and
  *  provides access to its sections.
@@ -48,12 +47,35 @@ public interface Partial {
         TYPES
     }
 
-    /** The name of this partial */
-    @NotNull String getName();
+    /**
+     * Returns the partial info.
+     *
+     * @return the partial info
+     */
+    @NotNull PartialInfo getPartialInfo();
 
     /** Return a specific section of the partial, by name */
     @NotNull Optional<Section> getSection(SectionName name);
 
     /** Names of the Partials on which this one depends */
-    @NotNull Set<String> getRequiredPartialNames();
+    @NotNull Set<PartialInfo> getRequiredPartialNames();
+
+    /**
+     * <p>
+     * Returns the digest of the source that was used to build this partial. Implementations should output this using the following format:
+     * <pre>
+     * algorithm: digest
+     * </pre>
+     * where the algorithm has to be one of the standard names defined in the
+     * <a href="https://docs.oracle.com/en/java/javase/11/docs/specs/security/standard-names.html#messagedigest-algorithms">Java Security Standard Algorithm Names</a>.
+     * </p>
+     * <p>A SHA-256 digest would have, for example, the following format:
+     * <pre>
+     * SHA-256: 703bd06e9d65118c75abe9a7a06f6a2fcdb8a19ef62d994f4cc1be0b34420383
+     * </pre>
+     * </p>
+     * @return the digest of the source that was used to build this partial
+     */
+    @NotNull String getDigest();
+
 }
diff --git a/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/PartialInfo.java b/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/PartialInfo.java
index 6b18bee..1ddf71c 100644
--- a/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/PartialInfo.java
+++ b/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/PartialInfo.java
@@ -22,6 +22,7 @@ import java.net.URL;
 import java.nio.file.Path;
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.Objects;
 import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -32,7 +33,7 @@ import org.osgi.framework.Version;
 /**
  * This class provides some utility methods to extract information about a partial without parsing it.
  */
-public final class PartialInfo {
+public final class PartialInfo implements Comparable<PartialInfo> {
 
     private static final String PARTIAL_NAME_AND_VERSION_REGEX = "([a-z][a-zA-Z0-9_\\.]*)(-(\\d\\.\\d\\.\\d))?";
 
@@ -72,6 +73,40 @@ public final class PartialInfo {
         return version;
     }
 
+    @Override
+    public int compareTo(@NotNull PartialInfo o) {
+        if (this.equals(o)) {
+            return 0;
+        }
+        int nameComparison = this.name.compareTo(o.name);
+        if (nameComparison == 0) {
+            return version.compareTo(o.version);
+        }
+        return nameComparison;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(name, version);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj instanceof PartialInfo) {
+            PartialInfo other = (PartialInfo) obj;
+            return Objects.equals(name, other.name) && Objects.equals(version, other.version);
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return name + (version == Version.emptyVersion ? "" : "-" + version);
+    }
+
     /**
      * Parses a {@code path} and returns a {@link PartialInfo}.
      *
diff --git a/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/PartialReader.java b/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/PartialReader.java
index 54f13e5..b898317 100644
--- a/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/PartialReader.java
+++ b/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/PartialReader.java
@@ -20,17 +20,19 @@ package org.apache.sling.graphql.schema.aggregator.impl;
 
 import java.io.IOException;
 import java.io.Reader;
+import java.nio.charset.StandardCharsets;
 import java.util.Collections;
 import java.util.EnumMap;
-import java.util.HashSet;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
-import java.util.stream.Stream;
 import java.util.function.Supplier;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import org.apache.commons.codec.binary.Hex;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.io.IOUtils;
 import org.apache.commons.io.input.BoundedReader;
 import org.jetbrains.annotations.NotNull;
 
@@ -44,8 +46,9 @@ public class PartialReader implements Partial {
     private static final int EOL = '\n';
 
     private final Map<SectionName, Section> sections = new EnumMap<>(SectionName.class);
-    private final String name;
-    private final Set<String> requiredPartialNames;
+    private final PartialInfo partialInfo;
+    private final Set<PartialInfo> requiredPartialNames;
+    private final String digest;
 
     /** The PARTIAL section is the only required one */
     public static final String PARTIAL_SECTION = "PARTIAL";
@@ -90,20 +93,15 @@ public class PartialReader implements Partial {
     }
     
     public PartialReader(@NotNull PartialInfo partialInfo, @NotNull Supplier<Reader> source) throws IOException {
-        this.name = partialInfo.getName();
+        this.partialInfo = partialInfo;
         parse(source);
+        this.digest = "SHA-256: " + Hex.encodeHexString(
+                DigestUtils.updateDigest(DigestUtils.getSha256Digest(), IOUtils.toByteArray(source.get(), StandardCharsets.UTF_8)).digest());
         final Partial.Section requirements = sections.get(SectionName.REQUIRES);
         if(requirements == null) {
             requiredPartialNames = Collections.emptySet();
         } else {
-            requiredPartialNames = new HashSet<>();
-            Stream.of(
-                requirements.getDescription().split(",")
-            )
-                .map(String::trim)
-                .filter(s -> !s.isEmpty())
-                .forEach(requiredPartialNames::add)
-            ;
+            requiredPartialNames = PartialInfo.fromRequiresSection(requirements.getDescription());
         }
     }
 
@@ -168,18 +166,23 @@ public class PartialReader implements Partial {
     }
 
     @Override
+    public @NotNull PartialInfo getPartialInfo() {
+        return partialInfo;
+    }
+
+    @Override
     public @NotNull Optional<Section> getSection(Partial.SectionName name) {
         final Section s = sections.get(name);
         return Optional.ofNullable(s);
     }
 
     @Override
-    public @NotNull String getName() {
-        return name;
+    public @NotNull Set<PartialInfo> getRequiredPartialNames() {
+        return Collections.unmodifiableSet(requiredPartialNames);
     }
 
     @Override
-    public @NotNull Set<String> getRequiredPartialNames() {
-        return requiredPartialNames;
+    public @NotNull String getDigest() {
+        return digest;
     }
-}
\ No newline at end of file
+}
diff --git a/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/ProviderBundleTracker.java b/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/ProviderBundleTracker.java
index 09c7b31..e016ae1 100644
--- a/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/ProviderBundleTracker.java
+++ b/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/ProviderBundleTracker.java
@@ -55,7 +55,7 @@ public class ProviderBundleTracker implements BundleTrackerCustomizer<Object> {
     public static final String SCHEMA_PATH_HEADER = "Sling-GraphQL-Schema";
 
     private final Logger log = LoggerFactory.getLogger(getClass().getName());
-    private final Map<String, BundleEntryPartial> schemaProviders = new ConcurrentHashMap<>();
+    private final Map<PartialInfo, BundleEntryPartial> schemaProviders = new ConcurrentHashMap<>();
 
     private BundleContext bundleContext;
 
@@ -96,11 +96,11 @@ public class ProviderBundleTracker implements BundleTrackerCustomizer<Object> {
 
     private void addIfNotPresent(BundleEntryPartial a) {
         if(a != null) {
-            if(schemaProviders.containsKey(a.getName())) {
-                log.warn("Partial provider with name {} already present, new one will be ignored", a.getName());
+            if(schemaProviders.containsKey(a.getPartialInfo())) {
+                log.warn("Partial provider for partial {} already present, new one will be ignored", a.getPartialInfo());
             } else {
                 log.info("Registering {}", a);
-                schemaProviders.put(a.getName(), a);
+                schemaProviders.put(a.getPartialInfo(), a);
             }
         }
     }
@@ -121,7 +121,7 @@ public class ProviderBundleTracker implements BundleTrackerCustomizer<Object> {
         // do nothing
     }
 
-    Map<String, Partial> getSchemaProviders() {
+    Map<PartialInfo, Partial> getSchemaProviders() {
         return Collections.unmodifiableMap(schemaProviders);
     }
 }
diff --git a/src/test/java/org/apache/sling/graphql/schema/aggregator/impl/DefaultSchemaAggregatorTest.java b/src/test/java/org/apache/sling/graphql/schema/aggregator/impl/DefaultSchemaAggregatorTest.java
index c0268eb..005274d 100644
--- a/src/test/java/org/apache/sling/graphql/schema/aggregator/impl/DefaultSchemaAggregatorTest.java
+++ b/src/test/java/org/apache/sling/graphql/schema/aggregator/impl/DefaultSchemaAggregatorTest.java
@@ -18,11 +18,6 @@
  */
 package org.apache.sling.graphql.schema.aggregator.impl;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertThrows;
-import static org.junit.Assert.assertTrue;
-
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.StringWriter;
@@ -30,10 +25,6 @@ import java.lang.reflect.Field;
 import java.util.Optional;
 import java.util.stream.Stream;
 
-import graphql.language.TypeDefinition;
-import graphql.schema.idl.SchemaParser;
-import graphql.schema.idl.TypeDefinitionRegistry;
-
 import org.apache.commons.io.IOUtils;
 import org.apache.sling.graphql.schema.aggregator.U;
 import org.junit.Before;
@@ -41,6 +32,14 @@ import org.junit.Test;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
 
+import graphql.language.TypeDefinition;
+import graphql.schema.idl.SchemaParser;
+import graphql.schema.idl.TypeDefinitionRegistry;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
@@ -74,7 +73,7 @@ public class DefaultSchemaAggregatorTest {
     }
 
     @Test
-    public void noProviders() throws Exception{
+    public void noProviders() {
         final StringWriter target = new StringWriter();
         final IOException iox = assertThrows(IOException.class, () -> dsa.aggregate(target, "Aprov", "Bprov"));
         assertContainsIgnoreCase("missing providers", iox.getMessage());
@@ -175,6 +174,25 @@ public class DefaultSchemaAggregatorTest {
    }
 
     @Test
+    public void versionedPartials() throws IOException {
+        final StringWriter target = new StringWriter();
+        tracker.addingBundle(U.mockProviderBundle(bundleContext, "required.partials", 1, "required-1.0.0.txt"), null);
+        tracker.addingBundle(U.mockProviderBundle(bundleContext, "versioned.partials", 2, "versioned-1.0.0.txt"), null);
+        dsa.aggregate(target, "versioned-1.0.0");
+        assertOutput("/partials/versionedPartials-output.txt", target.toString());
+    }
+
+    @Test
+    public void versionedPartialsMissingCorrectVersion() throws IOException {
+        final StringWriter target = new StringWriter();
+        tracker.addingBundle(U.mockProviderBundle(bundleContext, "required.partials", 1, "required-1.0.0.txt"), null);
+        tracker.addingBundle(U.mockProviderBundle(bundleContext, "versioned.partials", 2, "versioned-2.0.0.txt"), null);
+        final IOException iox = assertThrows(IOException.class, () -> dsa.aggregate(target, "versioned-2.0.0"));
+        assertContainsIgnoreCase("Missing providers", iox.getMessage());
+        assertContainsIgnoreCase("required-2.0.0", iox.getMessage());
+    }
+
+    @Test
     public void cycleInRequirements() throws Exception {
         final StringWriter target = new StringWriter();
         tracker.addingBundle(U.mockProviderBundle(bundleContext, "SDL", 1, "circularA.txt", "circularB.txt"), null);
diff --git a/src/test/java/org/apache/sling/graphql/schema/aggregator/impl/PartialReaderTest.java b/src/test/java/org/apache/sling/graphql/schema/aggregator/impl/PartialReaderTest.java
index 6e031f5..615fc3e 100644
--- a/src/test/java/org/apache/sling/graphql/schema/aggregator/impl/PartialReaderTest.java
+++ b/src/test/java/org/apache/sling/graphql/schema/aggregator/impl/PartialReaderTest.java
@@ -148,4 +148,13 @@ public class PartialReaderTest {
         assertTrue("Expecting requires section", p.getSection(Partial.SectionName.REQUIRES).isPresent());
         assertEquals("[a.sdl, b.sdl]", p.getRequiredPartialNames().toString());
     }
+
+    @Test
+    public void testDigest() throws IOException {
+        final PartialReader p = new PartialReader(
+                PartialInfo.fromPath(Paths.get("/partials/versioned-1.0.0.txt")),
+                getResourceReaderSupplier("/partials/versioned-1.0.0.txt")
+        );
+        assertEquals("SHA-256: 703bd06e9d65118c75abe9a7a06f6a2fcdb8a19ef62d994f4cc1be0b34420383", p.getDigest());
+    }
 }
diff --git a/src/test/java/org/apache/sling/graphql/schema/aggregator/impl/ProviderBundleTrackerTest.java b/src/test/java/org/apache/sling/graphql/schema/aggregator/impl/ProviderBundleTrackerTest.java
index 2ceee73..d8a57c4 100644
--- a/src/test/java/org/apache/sling/graphql/schema/aggregator/impl/ProviderBundleTrackerTest.java
+++ b/src/test/java/org/apache/sling/graphql/schema/aggregator/impl/ProviderBundleTrackerTest.java
@@ -83,7 +83,7 @@ public class ProviderBundleTrackerTest {
         final Bundle b = U.mockProviderBundle(bundleContext, "B", ++bundleId, "tt.txt", "another.x");
         tracker.addingBundle(a, null);
         tracker.addingBundle(b, null);
-        capture.assertContains(Level.WARN, "Partial provider with name tt already present");
+        capture.assertContains(Level.WARN, "Partial provider for partial tt already present");
         assertEquals(2, tracker.getSchemaProviders().size());
     }
 
diff --git a/src/test/java/org/apache/sling/graphql/schema/aggregator/it/SchemaAggregatorTestSupport.java b/src/test/java/org/apache/sling/graphql/schema/aggregator/it/SchemaAggregatorTestSupport.java
index 8163c41..72b02c2 100644
--- a/src/test/java/org/apache/sling/graphql/schema/aggregator/it/SchemaAggregatorTestSupport.java
+++ b/src/test/java/org/apache/sling/graphql/schema/aggregator/it/SchemaAggregatorTestSupport.java
@@ -85,6 +85,7 @@ public abstract class SchemaAggregatorTestSupport extends TestSupport {
                 .put("whitelist.bundles.regexp", "^PAXEXAM.*$")
                 .asOption(),
             mavenBundle().groupId("org.apache.sling").artifactId("org.apache.sling.servlet-helpers").versionAsInProject(),
+            mavenBundle().groupId("commons-codec").artifactId("commons-codec").versionAsInProject(),
             junitBundles()
         );
     }
@@ -147,4 +148,4 @@ public abstract class SchemaAggregatorTestSupport extends TestSupport {
     protected String getContent(String path) throws Exception {
         return executeRequest("GET", path, null, null, null, 200).getOutputAsString();
     }
-}
\ No newline at end of file
+}
diff --git a/src/test/resources/partials/required-1.0.0.txt b/src/test/resources/partials/required-1.0.0.txt
new file mode 100644
index 0000000..8e4ecef
--- /dev/null
+++ b/src/test/resources/partials/required-1.0.0.txt
@@ -0,0 +1,4 @@
+PARTIAL: A partial that will be required by another.
+
+PROLOGUE:
+This section should be present at the top of the generated schema file.
diff --git a/src/test/resources/partials/versioned-1.0.0.txt b/src/test/resources/partials/versioned-1.0.0.txt
new file mode 100644
index 0000000..5bf71b7
--- /dev/null
+++ b/src/test/resources/partials/versioned-1.0.0.txt
@@ -0,0 +1,3 @@
+PARTIAL: Testing a versioned partial
+
+REQUIRES: required-1.0.0
diff --git a/src/test/resources/partials/versioned-2.0.0.txt b/src/test/resources/partials/versioned-2.0.0.txt
new file mode 100644
index 0000000..e2e5866
--- /dev/null
+++ b/src/test/resources/partials/versioned-2.0.0.txt
@@ -0,0 +1,3 @@
+PARTIAL: Testing a versioned partial
+
+REQUIRES: required-2.0.0
diff --git a/src/test/resources/partials/versionedPartials-output.txt b/src/test/resources/partials/versionedPartials-output.txt
new file mode 100644
index 0000000..bf642f0
--- /dev/null
+++ b/src/test/resources/partials/versionedPartials-output.txt
@@ -0,0 +1,10 @@
+# Schema aggregated by DefaultSchemaAggregator
+
+# DefaultSchemaAggregator.source=required-1.0.0
+This section should be present at the top of the generated schema file.
+
+type Query {
+
+}
+
+# End of Schema aggregated from {versioned-1.0.0,required-1.0.0} by DefaultSchemaAggregator