You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by bd...@apache.org on 2021/07/15 15:58:09 UTC
[sling-org-apache-sling-graphql-schema-aggregator] 01/01:
SLING-10551 - initial commit, moving from sling-whiteboard (commit c0219939)
This is an automated email from the ASF dual-hosted git repository.
bdelacretaz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-graphql-schema-aggregator.git
commit b8d51775c81ca8b00fe58b800bfd59f04dd043c5
Author: Bertrand Delacretaz <bd...@apache.org>
AuthorDate: Thu Jul 15 17:55:46 2021 +0200
SLING-10551 - initial commit, moving from sling-whiteboard (commit c0219939)
---
.asf.yaml | 7 +
.gitignore | 12 +
CODE_OF_CONDUCT.md | 22 ++
CONTRIBUTING.md | 24 ++
Jenkinsfile | 20 ++
README.md | 121 +++++++++
pom.xml | 298 +++++++++++++++++++++
.../schema/aggregator/api/SchemaAggregator.java | 42 +++
.../schema/aggregator/api/package-info.java | 22 ++
.../impl/BundleEntryPartialProvider.java | 89 ++++++
.../aggregator/impl/DefaultSchemaAggregator.java | 173 ++++++++++++
.../graphql/schema/aggregator/impl/Partial.java | 47 ++++
.../schema/aggregator/impl/PartialConstants.java | 28 ++
.../schema/aggregator/impl/PartialReader.java | 173 ++++++++++++
.../aggregator/impl/ProviderBundleTracker.java | 127 +++++++++
.../schema/aggregator/impl/URLReaderSupplier.java | 47 ++++
.../servlet/SchemaAggregatorServlet.java | 162 +++++++++++
.../graphql/schema/aggregator/LogCapture.java | 70 +++++
.../apache/sling/graphql/schema/aggregator/U.java | 134 +++++++++
.../impl/BundleEntryPartialProviderTest.java | 34 +++
.../schema/aggregator/impl/CapitalizeTest.java | 43 +++
.../impl/DefaultSchemaAggregatorTest.java | 175 ++++++++++++
.../schema/aggregator/impl/PartialReaderTest.java | 129 +++++++++
.../aggregator/impl/ProviderBundleTrackerTest.java | 103 +++++++
.../impl/SchemaAggregatorServletTest.java | 58 ++++
.../aggregator/it/SchemaAggregatorServletIT.java | 74 +++++
.../aggregator/it/SchemaAggregatorTestSupport.java | 150 +++++++++++
src/test/resources/logback.xml | 31 +++
src/test/resources/partials/a.sdl.txt | 21 ++
src/test/resources/partials/b.sdl.txt | 10 +
src/test/resources/partials/c.sdl.txt | 10 +
src/test/resources/partials/circularA.txt | 6 +
src/test/resources/partials/circularB.txt | 6 +
.../partials/duplicate.section.partial.txt | 6 +
src/test/resources/partials/example.partial.txt | 27 ++
src/test/resources/partials/utf8.partial.txt | 2 +
src/test/resources/several-providers-output.txt | 16 ++
37 files changed, 2519 insertions(+)
diff --git a/.asf.yaml b/.asf.yaml
new file mode 100644
index 0000000..7a30853
--- /dev/null
+++ b/.asf.yaml
@@ -0,0 +1,7 @@
+github:
+ description: "Apache Sling GraphQL Schema Aggregator"
+ homepage: "https://sling.apache.org/"
+ labels:
+ - sling
+ - java
+ - graphql
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..08082a5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+.project
+.classpath
+.settings
+target
+bin
+*.iml
+.idea
+.DS_Store
+dependency-reduced-pom.xml
+.vscode
+node_modules
+openwhisk_action.zip
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..0fa18e5
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,22 @@
+<!--/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/-->
+Apache Software Foundation Code of Conduct
+====
+
+Being an Apache project, Apache Sling adheres to the Apache Software Foundation's [Code of Conduct](https://www.apache.org/foundation/policies/conduct.html).
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..ac82a1a
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,24 @@
+<!--/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/-->
+Contributing
+====
+
+Thanks for choosing to contribute!
+
+You will find all the necessary details about how you can do this at https://sling.apache.org/contributing.html.
diff --git a/Jenkinsfile b/Jenkinsfile
new file mode 100644
index 0000000..f582519
--- /dev/null
+++ b/Jenkinsfile
@@ -0,0 +1,20 @@
+/**
+ * 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.
+ */
+
+slingOsgiBundleBuild()
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0e5d6f3
--- /dev/null
+++ b/README.md
@@ -0,0 +1,121 @@
+[![Apache Sling](https://sling.apache.org/res/logos/sling.png)](https://sling.apache.org)
+
+ [![Build Status](https://ci-builds.apache.org/job/Sling/job/modules/job/sling-org-apache-sling-graphql-schema-aggregator/job/master/badge/icon)](https://ci-builds.apache.org/job/Sling/job/modules/job/sling-org-apache-sling-graphql-schema-aggregator/job/master/) [![Test Status](https://img.shields.io/jenkins/tests.svg?jobUrl=https://ci-builds.apache.org/job/Sling/job/modules/job/sling-org-apache-sling-graphql-schema-aggregator/job/master/)](https://ci-builds.apache.org/job/Sling/ [...]
+
+Apache Sling GraphQL Schema Aggregator
+----
+
+_This module is one of several which provide [GraphQL support for Apache Sling](https://github.com/search?q=topic%3Asling+topic%3Agraphql+org%3Aapache&type=Repositories)._
+
+The Sling GraphQL Schema Aggregator provides services to combine partial GraphQL
+schema ("partials") supplied by _provider bundles_.
+
+The partials are structured text files, supplied as OSGi bundle resources, that provide sections (like query,
+mutation, types sections) that are aggregated to build a GraphQL Schema using the SDL (Schema
+Definition Language) syntax.
+
+A GraphQL schema must contain one `Query` statement and can contain a most one `Mutation` statement,
+so partials cannot be assembled by just concatenating them. The schema assembler defines a simple
+section-based syntax for the partials, so that they can be aggregated efficiently.
+
+This module also provides a `SchemaAggregatorServlet` that generates schemas by aggregating partials, by
+mapping request selectors to lists of partial names. The result can be used directly by the Sling GraphQL
+Core module, which makes an internal Sling request to get the schema.
+
+Partials can also depend on others by declaring the required dependencies by name, to make sure the
+aggregated schemas are valid.
+
+With this mechanism, an OSGi bundle can provide both a partial schema and the Sling data fetching and
+processing services that go with it. This allows a GraphQL "API plane" (usually defined by a specific
+instance of the Sling `GraphQLServlet`) to be built out of several OSGi bundles which each focus on a
+specific set of queries, mutations and types.
+
+## Provider bundles
+
+To provide partials, a bundle sets a `Sling-GraphQL-Schema` header in its OSGi manifest, with a value that
+points to a path under which partials are found in the bundle resources.
+
+A partial is a text file with the structure described below. As usual, The Truth Is In The Tests, see
+the [example partial in the test sources](./src/test/resources/partials/example.partial.txt) for a
+reference that's guaranteed to be valid.
+
+ # Example GraphQL partial schema
+ # Any text before the first section is ignored.
+
+ PARTIAL: Example GraphQL schema partial
+ The contents of the PARTIAL section are ignored, only its
+ description (the text follows the PARTIAL section name
+ above) is used.
+
+ PARTIAL is the only required section.
+
+ REQUIRE: base.scalars, base.schema
+ The description of the optional REQUIRE section is a
+ comma-separated list of partials which are required for this
+ one to be valid. The content of this section is ignored, only
+ its description is used to build that list.
+
+ PROLOGUE:
+ The content of the optional PROLOGUE section is concatenated
+ in the aggregated schema, before all the other sections.
+
+ QUERY:
+ The content of the optional QUERY sections of all partials
+ is aggregated in a `type QUERY {...}` section in the output.
+
+ MUTATION:
+ Like for the QUERY section, the content of the optional
+ MUTATION sections of all partials is aggregated in
+ a `type MUTATION {...}` section in the output.
+
+ TYPES:
+ The content of the TYPES sections of all partials is
+ aggregated in the output, after all the other sections.
+
+## Partial names
+
+The name of a partial, used in the selector mappings of the
+`SchemaAggregatorServlet`, is defined by its filename in the
+bundle resources, omitting the file extension. A partial
+found under `/path-set-by-the-bundle-header/this.is.txt` in its bundle is named
+`this.is` . Partial names must be unique system-wide, so it's
+good to use some form of namespacing or agreed upon naming
+convention for them.
+
+## SchemaAggregatorServlet configuration
+Here's a configuration example from the test code.
+
+ // Configure the org.apache.sling.graphql.schema.aggregator.SchemaAggregatorServlet
+ factoryConfiguration(AGGREGATOR_SERVLET_CONFIG_PID)
+ .put("sling.servlet.resourceTypes", "sling/servlet/default")
+
+ // The extension must be the one used by the GraphQLServlet to retrieve schemas
+ // which by default is 'GQLschema'
+ .put("sling.servlet.extensions", GQL_SCHEMA_EXT)
+
+ // The GraphQLServlet uses an internal GET request for the schema
+ .put("sling.servlet.methods", new String[] { "GET" })
+
+ // Several selectors can be configured to setup API planes, each with their own GraphQL schema
+ .put("sling.servlet.selectors", new String[] { "X", "Y" })
+
+ // This mapping defines which partials to use to build the schema for each selector
+ // The lists can use either the exact names of partials, or (Java flavored) regular expressions on
+ // their names, identified by a starting and ending slash.
+ .put("selectors.to.partials.mapping", new String[] { "X:firstA,secondB", "Y:secondA,firstB,/second.*/" })
+
+## TODO / wishlist
+Invalid section names in partials should cause parsing errors.
+
+The REQUIRES section of partial should be translated to OSGi capabilities, to be able to detect
+missing requirements at system assembly time or using the
+[Feature Model Analyser](https://github.com/apache/sling-org-apache-sling-feature-analyser).
+
+We'll probably need a utility to aggregate schemas for automated tests, to allow test code
+to include required schema partials.
+
+Errors like invalid or missing partials are currently only logged, it would be useful to
+have them cause louder errors, like schema aggregation failing with error messages when
+things went wrong, and/or this module providing a Health Check service to detect problems.
+
+Caching is probably not needed in this module, as the GraphQL Core caches compiled schemas.
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..cd56bb3
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,298 @@
+<?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>
+
+ <parent>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>sling-bundle-parent</artifactId>
+ <version>43</version>
+ <relativePath />
+ </parent>
+
+ <artifactId>org.apache.sling.graphql.schema.aggregator</artifactId>
+ <version>0.0.1-SNAPSHOT</version>
+
+ <name>Apache Sling GraphQL Schema Aggregator</name>
+ <description>Builds GraphQL Schemas from partials provided by OSGi bundles</description>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+ <org.ops4j.pax.exam.version>4.13.3</org.ops4j.pax.exam.version>
+ <site.javadoc.exclude>org.apache.sling.graphql.schema.aggregator.*</site.javadoc.exclude>
+ <!-- additional options that can be passed to Pax before executing the tests -->
+ <pax.vm.options />
+ </properties>
+
+ <scm>
+ <connection>scm:git:https://gitbox.apache.org/repos/asf/sling-org-apache-sling-graphql-schema-aggregator.git</connection>
+ <developerConnection>scm:git:https://gitbox.apache.org/repos/asf/sling-org-apache-sling-graphql-schema-aggregator.git</developerConnection>
+ <url>https://gitbox.apache.org/repos/asf?p=sling-org-apache-sling-graphql-schema-aggregator.git</url>
+ <tag>org.apache.sling.graphql.core-0.0.2</tag>
+ </scm>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>biz.aQute.bnd</groupId>
+ <artifactId>bnd-maven-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <groupId>biz.aQute.bnd</groupId>
+ <artifactId>bnd-baseline-maven-plugin</artifactId>
+ <configuration>
+ <!-- TODO remove this once we have a release of this module -->
+ <failOnMissing>false</failOnMissing>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-failsafe-plugin</artifactId>
+ <executions>
+ <execution>
+ <goals>
+ <goal>integration-test</goal>
+ <goal>verify</goal>
+ </goals>
+ </execution>
+ </executions>
+ <configuration>
+ <systemPropertyVariables>
+ <bundle.filename>${basedir}/target/${project.build.finalName}.jar</bundle.filename>
+ <pax.vm.options>${pax.vm.options}</pax.vm.options>
+ </systemPropertyVariables>
+ <redirectTestOutputToFile>true</redirectTestOutputToFile>
+ <!-- pax exam bug, often times out at exit -->
+ <forkedProcessExitTimeoutInSeconds>1</forkedProcessExitTimeoutInSeconds>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.servicemix.tooling</groupId>
+ <artifactId>depends-maven-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.rat</groupId>
+ <artifactId>apache-rat-plugin</artifactId>
+ <configuration>
+ <excludes>
+ <exclude>src/test/resources/several-providers-output.txt</exclude>
+ <exclude>src/test/resources/partials/**</exclude>
+ </excludes>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>osgi.core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>osgi.cmpn</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.service.component.annotations</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.service.metatype.annotations</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.annotation.versioning</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.annotation.bundle</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.apache.felix.framework</artifactId>
+ <version>6.0.3</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.jetbrains</groupId>
+ <artifactId>annotations</artifactId>
+ <version>16.0.3</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.api</artifactId>
+ <version>2.18.4</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.engine</artifactId>
+ <version>2.6.22</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.resource.presence</artifactId>
+ <version>0.0.2</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ <version>2.10.0</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.servlet-helpers</artifactId>
+ <version>1.4.2</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-inline</artifactId>
+ <version>3.5.11</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.testing.sling-mock.junit4</artifactId>
+ <version>2.4.0</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>javax.inject</groupId>
+ <artifactId>javax.inject</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.testing.paxexam</artifactId>
+ <version>3.1.0</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.ops4j.pax.exam</groupId>
+ <artifactId>pax-exam</artifactId>
+ <version>${org.ops4j.pax.exam.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.ops4j.pax.exam</groupId>
+ <artifactId>pax-exam-cm</artifactId>
+ <version>${org.ops4j.pax.exam.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.ops4j.pax.exam</groupId>
+ <artifactId>pax-exam-container-forked</artifactId>
+ <version>${org.ops4j.pax.exam.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.ops4j.pax.exam</groupId>
+ <artifactId>pax-exam-junit4</artifactId>
+ <version>${org.ops4j.pax.exam.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.ops4j.pax.exam</groupId>
+ <artifactId>pax-exam-link-mvn</artifactId>
+ <version>${org.ops4j.pax.exam.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.ops4j.pax.url</groupId>
+ <artifactId>pax-url-wrap</artifactId>
+ <version>2.3.0</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.servicemix.bundles</groupId>
+ <artifactId>org.apache.servicemix.bundles.hamcrest</artifactId>
+ <version>1.3_1</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient-osgi</artifactId>
+ <version>4.5.10</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-classic</artifactId>
+ <version>1.2.3</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.graphql-java</groupId>
+ <artifactId>graphql-java</artifactId>
+ <version>15.0</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <reporting>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-javadoc-plugin</artifactId>
+ <reportSets>
+ <reportSet>
+ <reports>
+ <report>javadoc</report>
+ </reports>
+ </reportSet>
+ </reportSets>
+ <configuration>
+ <stylesheet>maven</stylesheet>
+ <excludePackageNames>*.core:*.impl:*.internal:${site.javadoc.exclude}</excludePackageNames>
+ </configuration>
+ </plugin>
+ </plugins>
+ </reporting>
+
+</project>
diff --git a/src/main/java/org/apache/sling/graphql/schema/aggregator/api/SchemaAggregator.java b/src/main/java/org/apache/sling/graphql/schema/aggregator/api/SchemaAggregator.java
new file mode 100644
index 0000000..1667669
--- /dev/null
+++ b/src/main/java/org/apache/sling/graphql/schema/aggregator/api/SchemaAggregator.java
@@ -0,0 +1,42 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+
+package org.apache.sling.graphql.schema.aggregator.api;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import org.jetbrains.annotations.NotNull;
+import org.osgi.annotation.versioning.ProviderType;
+
+@ProviderType
+public interface SchemaAggregator {
+ /** Aggregate the schemas supplied by partial schema providers which match the exact names
+ * or patterns supplied.
+ *
+ * @param target where to write the output
+ *
+ * @param providerNamesOrRegexp a value that starts and ends with a slash is used a a regular
+ * expression to match provider names (after removing the starting and ending slash), other
+ * values are used as exact provider names, which are then required.
+ *
+ * @throws IOException if an exact provider name is not found
+ */
+ void aggregate(@NotNull Writer target, @NotNull String ... providerNamesOrRegexp) throws IOException;
+}
diff --git a/src/main/java/org/apache/sling/graphql/schema/aggregator/api/package-info.java b/src/main/java/org/apache/sling/graphql/schema/aggregator/api/package-info.java
new file mode 100644
index 0000000..cd0895d
--- /dev/null
+++ b/src/main/java/org/apache/sling/graphql/schema/aggregator/api/package-info.java
@@ -0,0 +1,22 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+@Version("0.0.1")
+package org.apache.sling.graphql.schema.aggregator.api;
+
+import org.osgi.annotation.versioning.Version;
diff --git a/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/BundleEntryPartialProvider.java b/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/BundleEntryPartialProvider.java
new file mode 100644
index 0000000..cb522bd
--- /dev/null
+++ b/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/BundleEntryPartialProvider.java
@@ -0,0 +1,89 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+
+package org.apache.sling.graphql.schema.aggregator.impl;
+
+import java.io.IOException;
+import java.net.URL;
+
+import org.osgi.framework.Bundle;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** {@PartialSchemaProvider} build out of a Bundle entry, which must be a valid
+ * partial schema file.
+ */
+class BundleEntryPartialProvider extends PartialReader implements Comparable<BundleEntryPartialProvider> {
+ private static final Logger log = LoggerFactory.getLogger(BundleEntryPartialProvider.class.getName());
+ private final String key;
+ private final long bundleId;
+
+ private BundleEntryPartialProvider(Bundle b, URL bundleEntry) throws IOException {
+ super(getPartialName(bundleEntry), new URLReaderSupplier(bundleEntry));
+ this.bundleId = b.getBundleId();
+ this.key = String.format("%s(%d):%s", b.getSymbolicName(), b.getBundleId(), bundleEntry.toString());
+ }
+
+ /** The partial's name is whatever follows the last slash, excluding the file extension */
+ static String getPartialName(URL url) {
+ final String [] parts = url.toString().split("/");
+ String result = parts[parts.length - 1];
+ final int lastDot = result.lastIndexOf(".");
+ return lastDot > 0 ? result.substring(0, lastDot) : result;
+ }
+
+ /** @return a BundleEntryPartialProvider for the entryPath in
+ * the supplied Bundle, or null if none can be built.
+ */
+ static BundleEntryPartialProvider forBundle(Bundle b, String entryPath) throws IOException {
+ final URL entry = b.getEntry(entryPath);
+ if(entry == null) {
+ log.info("Entry {} not found for bundle {}", entryPath, b.getSymbolicName());
+ return null;
+ } else {
+ return new BundleEntryPartialProvider(b, entry);
+ }
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if(other instanceof BundleEntryPartialProvider) {
+ return ((BundleEntryPartialProvider)other).key.equals(key);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return key.hashCode();
+ }
+
+ public String toString() {
+ return String.format("%s: %s", getClass().getSimpleName(), key);
+ }
+
+ public long getBundleId() {
+ return bundleId;
+ }
+
+ @Override
+ public int compareTo(BundleEntryPartialProvider o) {
+ return getName().compareTo(o.getName());
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..8942b20
--- /dev/null
+++ b/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/DefaultSchemaAggregator.java
@@ -0,0 +1,173 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+
+package org.apache.sling.graphql.schema.aggregator.impl;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.sling.graphql.schema.aggregator.api.SchemaAggregator;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.apache.sling.graphql.schema.aggregator.impl.PartialConstants.S_MUTATION;
+import static org.apache.sling.graphql.schema.aggregator.impl.PartialConstants.S_PROLOGUE;
+import static org.apache.sling.graphql.schema.aggregator.impl.PartialConstants.S_QUERY;
+import static org.apache.sling.graphql.schema.aggregator.impl.PartialConstants.S_TYPES;
+
+@Component(service = SchemaAggregator.class)
+public class DefaultSchemaAggregator implements SchemaAggregator {
+ private static final Logger log = LoggerFactory.getLogger(DefaultSchemaAggregator.class.getName());
+ public static final int MAX_REQUIREMENTS_RECURSION_LEVEL = 5;
+
+ @Reference
+ private ProviderBundleTracker tracker;
+
+ static String capitalize(String s) {
+ if(s == null) {
+ return null;
+ } else if(s.length() > 1) {
+ return String.format("%s%s", s.substring(0, 1).toUpperCase(), s.substring(1).toLowerCase());
+ } else {
+ return s.toUpperCase();
+ }
+ }
+
+ private void copySection(Set<Partial> selected, String sectionName, boolean inBlock, Writer target) throws IOException {
+ String prefixToWrite = inBlock ? String.format("%ntype %s {%n", capitalize(sectionName)) : null;
+ boolean anyOutput = false;
+ for(Partial p : selected) {
+ final Optional<Partial.Section> section = p.getSection(sectionName);
+ if(section.isPresent()) {
+ anyOutput = true;
+ if(prefixToWrite != null) {
+ target.write(prefixToWrite);
+ prefixToWrite = null;
+ }
+ writeSourceInfo(target, p);
+ IOUtils.copy(section.get().getContent(), target);
+ }
+ }
+ if(anyOutput && inBlock) {
+ target.write(String.format("%n}%n"));
+ }
+ }
+
+ private void writeSourceInfo(Writer target, Partial p) throws IOException {
+ target.write(String.format("%n# %s.source=%s%n", getClass().getSimpleName(), p.getName()));
+ }
+
+ @Override
+ public void aggregate(Writer target, String ...providerNamesOrRegexp) throws IOException {
+ final String info = String.format("Schema aggregated by %s%n", getClass().getSimpleName());
+ target.write(String.format("# %s", info));
+
+ // build list of selected providers
+ final Map<String, Partial> providers = tracker.getSchemaProviders();
+ if(log.isDebugEnabled()) {
+ log.debug("Aggregating schemas, request={}, providers={}", Arrays.asList(providerNamesOrRegexp), providers.keySet());
+ }
+ final Set<String> missing = new HashSet<>();
+ final Set<Partial> selected = selectProviders(providers, missing, providerNamesOrRegexp);
+
+ if(!missing.isEmpty()) {
+ log.debug("Requested providers {} not found in {}", missing, providers.keySet());
+ throw new IOException(String.format("Missing providers: %s", missing));
+ }
+
+ // copy sections that belong in the output SDL
+ copySection(selected, S_PROLOGUE, false, target);
+ copySection(selected, S_QUERY, true, target);
+ copySection(selected, S_MUTATION, true, target);
+ copySection(selected, S_TYPES, false, target);
+
+ final StringBuilder partialNames = new StringBuilder();
+ selected.forEach(p -> {
+ if(partialNames.length() > 0) {
+ partialNames.append(",");
+ }
+ partialNames.append(p.getName());
+ });
+ 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) {
+ 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()))
+ .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) {
+ 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) {
+
+ // 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()
+ ));
+ }
+
+ addTo.add(p);
+ for(String req : p.getRequiredPartialNames()) {
+ final Partial preq = providers.get(req);
+ if(preq == null) {
+ missing.add(req);
+ } else {
+ addWithRequirements(providers, addTo, missing, preq, recursionLevel + 1);
+ }
+ }
+ }
+
+ static Pattern toRegexp(String input) {
+ if(input.startsWith("/") && input.endsWith("/")) {
+ return Pattern.compile(input.substring(1, input.length() - 1));
+ }
+ return null;
+ }
+}
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
new file mode 100644
index 0000000..9b3d418
--- /dev/null
+++ b/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/Partial.java
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+package org.apache.sling.graphql.schema.aggregator.impl;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.util.Optional;
+import java.util.Set;
+
+/** Wrapper for the partials format, that parses a partial file and
+ * provides access to its sections.
+ * See the example.partial.txt and the tests for a description of
+ * the format.
+ */
+interface Partial {
+ /** A section in the partial */
+ interface Section {
+ String getName();
+ String getDescription();
+ Reader getContent() throws IOException;
+ }
+
+ /** The name of this partial */
+ String getName();
+
+ /** Return a specific section of the partial, by name */
+ Optional<Section> getSection(String name);
+
+ /** Names of the Partials on which this one depends */
+ Set<String> getRequiredPartialNames();
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/PartialConstants.java b/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/PartialConstants.java
new file mode 100644
index 0000000..aac50b2
--- /dev/null
+++ b/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/PartialConstants.java
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+package org.apache.sling.graphql.schema.aggregator.impl;
+
+class PartialConstants {
+ public static final String S_PARTIAL = "PARTIAL";
+ public static final String S_REQUIRES = "REQUIRES";
+ public static final String S_PROLOGUE = "PROLOGUE";
+ public static final String S_TYPES = "TYPES";
+ public static final String S_MUTATION = "MUTATION";
+ public static final String S_QUERY = "QUERY";
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..783a9d3
--- /dev/null
+++ b/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/PartialReader.java
@@ -0,0 +1,173 @@
+/*
+ * 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.
+ */
+package org.apache.sling.graphql.schema.aggregator.impl;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.util.Collections;
+import java.util.HashMap;
+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.io.IOUtils;
+import org.apache.commons.io.input.BoundedReader;
+
+/** Reader for the partials format, which parses a partial file and
+ * provides access to its sections.
+ * See the example.partial.txt and the tests for a description of
+ * the format.
+ */
+class PartialReader implements Partial {
+ private static final Pattern SECTION_LINE = Pattern.compile("([A-Z]+) *:(.*)");
+ private static final int EOL = '\n';
+
+ private final Map<String, Section> sections = new HashMap<>();
+ private final String name;
+ private final Set<String> requiredPartialNames;
+
+ /** The PARTIAL section is the only required one */
+ public static final String PARTIAL_SECTION = "PARTIAL";
+
+ static class SyntaxException extends IOException {
+ SyntaxException(String reason) {
+ super(reason);
+ }
+ }
+
+ static class ParsedSection implements Partial.Section {
+ private final Supplier<Reader> sectionSource;
+ private final String name;
+ private final String description;
+ private final int startCharIndex;
+ private final int endCharIndex;
+
+ ParsedSection(Supplier<Reader> sectionSource, String name, String description, int start, int end) {
+ this.sectionSource = sectionSource;
+ this.name = name;
+ this.description = description;
+ this.startCharIndex = start;
+ this.endCharIndex = end;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String getDescription() {
+ return description;
+ }
+
+ @Override
+ public Reader getContent() throws IOException {
+ final Reader r = sectionSource.get();
+ r.skip(startCharIndex);
+ return new BoundedReader(r, endCharIndex - startCharIndex);
+ }
+ }
+
+ PartialReader(String name, Supplier<Reader> source) throws IOException {
+ this.name = name;
+ parse(source);
+ final Partial.Section requirements = sections.get(PartialConstants.S_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)
+ ;
+ }
+ }
+
+ /* Detect lines that start with a <SECTION>: name
+ * in the input, and save them as sections
+ */
+ private void parse(Supplier<Reader> source) throws IOException {
+ final Reader input = source.get();
+ StringBuilder line = new StringBuilder();
+ int c;
+ int charCount = 0;
+ int lastSectionStart = 0;
+ String sectionName = null;
+ String sectionDescription = "";
+ while((c = input.read()) != -1) {
+ if(c == EOL) {
+ final Matcher m = SECTION_LINE.matcher(line);
+ if(m.matches()) {
+ // Add previous section
+ addSectionIfNameIsSet(source, sectionName, sectionDescription, lastSectionStart, charCount - line.length());
+ // And setup for the new section
+ sectionName = m.group(1).trim();
+ sectionDescription = m.group(2).trim();
+ lastSectionStart = charCount + 1;
+ }
+ line = new StringBuilder();
+ } else {
+ line.append((char)c);
+ }
+ charCount++;
+ }
+
+ // Add last section
+ addSectionIfNameIsSet(source, sectionName, sectionDescription, lastSectionStart, Integer.MAX_VALUE);
+
+ // And validate
+ if(!sections.containsKey(PARTIAL_SECTION)) {
+ throw new SyntaxException(String.format("Missing required %s section", PARTIAL_SECTION));
+ }
+
+ }
+
+ private void addSectionIfNameIsSet(Supplier<Reader> sectionSource, String name, String description, int start, int end) throws SyntaxException {
+ if(name != null) {
+ if(sections.containsKey(name)) {
+ throw new SyntaxException(String.format("Duplicate section %s", name));
+ }
+ sections.put(name, new ParsedSection(sectionSource, name, description, start, end));
+ }
+ }
+
+ @Override
+ public Optional<Section> getSection(String name) {
+ final Section s = sections.get(name);
+ return s == null ? Optional.empty() : Optional.of(s);
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public Set<String> getRequiredPartialNames() {
+ return requiredPartialNames;
+ }
+}
\ 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
new file mode 100644
index 0000000..7d38f9b
--- /dev/null
+++ b/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/ProviderBundleTracker.java
@@ -0,0 +1,127 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+
+package org.apache.sling.graphql.schema.aggregator.impl;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.osgi.annotation.bundle.Capability;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleEvent;
+import org.osgi.framework.wiring.BundleRevision;
+import org.osgi.framework.wiring.BundleWire;
+import org.osgi.framework.wiring.BundleWiring;
+import org.osgi.namespace.extender.ExtenderNamespace;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.util.tracker.BundleTracker;
+import org.osgi.util.tracker.BundleTrackerCustomizer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Tracks bundles which provide partial schemas and collects the corresponding set of schemas.
+ */
+@Component(
+ service = {ProviderBundleTracker.class}
+)
+@Capability(
+ namespace = ExtenderNamespace.EXTENDER_NAMESPACE,
+ name = "sling.graphql-schema-aggregator",
+ version = "0.1"
+)
+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, BundleEntryPartialProvider> schemaProviders = new ConcurrentHashMap<>();
+
+ private BundleContext bundleContext;
+
+ @Activate
+ public void activate(BundleContext bundleContext) {
+ this.bundleContext = bundleContext;
+ BundleTracker<?> bt = new BundleTracker<>(bundleContext, Bundle.ACTIVE, this);
+ bt.open();
+ }
+
+ @Override
+ public Object addingBundle(Bundle bundle, BundleEvent event) {
+ BundleWiring bundleWiring = bundle.adapt(BundleWiring.class);
+ Bundle us = bundleContext.getBundle();
+ if (bundleWiring.getRequiredWires(ExtenderNamespace.EXTENDER_NAMESPACE).stream().map(BundleWire::getProvider)
+ .map(BundleRevision::getBundle).anyMatch(us::equals)) {
+ final String providersPath = bundle.getHeaders().get(SCHEMA_PATH_HEADER);
+ if (providersPath == null) {
+ log.debug("Bundle {} has no {} header, ignored", bundle.getSymbolicName(), SCHEMA_PATH_HEADER);
+ } else {
+ // For now we only support file entries which are directly under providersPath
+ final Enumeration<String> paths = bundle.getEntryPaths(providersPath);
+ if (paths != null) {
+ while (paths.hasMoreElements()) {
+ final String path = paths.nextElement();
+ try {
+ addIfNotPresent(BundleEntryPartialProvider.forBundle(bundle, path));
+ } catch (IOException ioe) {
+ // TODO save errors and refuse to work if any happended?
+ log.error("Error reading partial " + path, ioe);
+ }
+ }
+ }
+ }
+ }
+ return bundle;
+ }
+
+ private void addIfNotPresent(BundleEntryPartialProvider a) {
+ if(a != null) {
+ if(schemaProviders.containsKey(a.getName())) {
+ log.warn("Partial provider with name {} already present, new one will be ignored", a.getName());
+ } else {
+ log.info("Registering {}", a);
+ schemaProviders.put(a.getName(), a);
+ }
+ }
+ }
+
+ @Override
+ public void removedBundle(Bundle bundle, BundleEvent event, Object object) {
+ final long id = bundle.getBundleId();
+ schemaProviders.forEach((key, value) -> {
+ if (id == value.getBundleId()) {
+ log.info("Removing {}", value);
+ schemaProviders.remove(key);
+ }
+ });
+ }
+
+ @Override
+ public void modifiedBundle(Bundle bundle, BundleEvent event, Object object) {
+ // do nothing
+ }
+
+ Map<String, Partial> getSchemaProviders() {
+ return Collections.unmodifiableMap(schemaProviders);
+ }
+}
diff --git a/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/URLReaderSupplier.java b/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/URLReaderSupplier.java
new file mode 100644
index 0000000..ec5c925
--- /dev/null
+++ b/src/main/java/org/apache/sling/graphql/schema/aggregator/impl/URLReaderSupplier.java
@@ -0,0 +1,47 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+
+package org.apache.sling.graphql.schema.aggregator.impl;
+
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.function.Supplier;
+
+class URLReaderSupplier implements Supplier<Reader> {
+ /** Partials must use this character set */
+ private static final Charset PARTIAL_CHARSET = StandardCharsets.UTF_8;
+
+ private final URL url;
+
+ URLReaderSupplier(URL url) {
+ this.url = url;
+ }
+
+ @Override
+ public Reader get() {
+ try {
+ return new InputStreamReader(url.openConnection().getInputStream(), PARTIAL_CHARSET);
+ } catch(Exception e) {
+ throw new RuntimeException("Error creating Reader for URL " + url, e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/graphql/schema/aggregator/servlet/SchemaAggregatorServlet.java b/src/main/java/org/apache/sling/graphql/schema/aggregator/servlet/SchemaAggregatorServlet.java
new file mode 100644
index 0000000..dc82945
--- /dev/null
+++ b/src/main/java/org/apache/sling/graphql/schema/aggregator/servlet/SchemaAggregatorServlet.java
@@ -0,0 +1,162 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+
+package org.apache.sling.graphql.schema.aggregator.servlet;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.Servlet;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.SlingHttpServletResponse;
+import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
+import org.apache.sling.graphql.schema.aggregator.api.SchemaAggregator;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.ConfigurationPolicy;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * <p>
+ * Servlet that aggregates GraphQL schemas provided by bundles that have a Sling-GraphQL-Schema header. The value of that header is a path
+ * under which bundle entries are found which follow the rules defined by our minimal parser to be used as partial GraphQL schemas
+ * ("partial").
+ * </p>
+ * <p>
+ * The name of such a partial is defined by its filename in the bundle, omitting the file extension. A partial found at /p/partial.1.txt is
+ * named 'partial.1', for example.
+ * </p>
+ * <p>
+ * The servlet must be mounted with at least one selector, and the first selector of the request is mapped to a configurable set of names or
+ * regular expressions used to select partial schema providers by name.
+ * </p>
+ * <p>
+ * If used with the Sling GraphQL Core module, the servlet must be configured with the 'GQLschema' extension as that module makes an
+ * internal request with this extension (+ selectors) to retrieve a GraphQL schema that uses the SDL syntax.
+ * </p>
+ * <p>
+ * Multiple selectors can be mapped to different sets of partial names or regular expressions. This can be used to define "API planes" which
+ * each have their own GraphQL schemas and are each addressed with a specific selector.
+ * </p>
+ */
+@Component(
+ service = Servlet.class,
+ name = "org.apache.sling.graphql.schema.aggregator.SchemaAggregatorServlet",
+ configurationPolicy=ConfigurationPolicy.REQUIRE,
+ property = {
+ "service.description=Sling GraphQL Schema Aggregator Servlet",
+ "service.vendor=The Apache Software Foundation"
+ })
+@Designate(ocd = SchemaAggregatorServlet.Config.class, factory=true)
+public class SchemaAggregatorServlet extends SlingSafeMethodsServlet {
+
+ private final transient Logger log = LoggerFactory.getLogger(getClass().getName());
+
+ @ObjectClassDefinition(
+ name = "Apache Sling GraphQL Schema Aggregator Servlet",
+ description = "Servlet that aggregates GraphQL schemas")
+ public @interface Config {
+ @AttributeDefinition(
+ name = "Selectors",
+ description="Standard Sling servlet property")
+ String[] sling_servlet_selectors() default "";
+
+ @AttributeDefinition(
+ name = "Resource Types",
+ description="Standard Sling servlet property")
+ String[] sling_servlet_resourceTypes() default "sling/servlet/default";
+
+ @AttributeDefinition(
+ name = "Methods",
+ description="Standard Sling servlet property")
+ String[] sling_servlet_methods() default "GET";
+
+ @AttributeDefinition(
+ name = "Extensions",
+ description="Standard Sling servlet property")
+ String[] sling_servlet_extensions() default "GQLschema";
+
+ @AttributeDefinition(
+ name = "Selectors to partials mapping",
+ description=
+ "Each entry is in the format S:P1,P2,... where S is the first selector of the incoming request "
+ + "and P* lists the names of the corresponding schema partials to use, "
+ + "and/or regular expressions such as /.*authoring.*/ to select all partials that match")
+ String[] selectors_to_partials_mapping() default {};
+
+ }
+
+ @Reference
+ private transient SchemaAggregator aggregator;
+
+ private Map<String, String[]> selectorsToPartialNames = new HashMap<>();
+
+ @Activate
+ public void activate(BundleContext ctx, Config cfg) {
+ for(String str : cfg.selectors_to_partials_mapping()) {
+ final String [] parts = str.split("[:,]");
+ if(parts.length < 2) {
+ log.warn("Invalid selectors_to_partials_mapping configuration string [{}]", str);
+ continue;
+ }
+ final String selector = parts[0].trim();
+ final String [] names = new String[parts.length - 1];
+ for(int i=1; i < parts.length; i++) {
+ names[i-1] = parts[i].trim();
+ }
+ if(log.isInfoEnabled()) {
+ log.info("Registering selector mapping: {} -> {}", selector, Arrays.asList(names));
+ }
+ selectorsToPartialNames.put(selector, names);
+ }
+ }
+
+ @Override
+ public void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException {
+ final String [] selectors = request.getRequestPathInfo().getSelectors();
+ if(selectors.length < 1) {
+ response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Missing required schema selector");
+ return;
+ }
+
+ response.setContentType("text/plain");
+ response.setCharacterEncoding("UTF-8");
+
+ final String key = selectors[0];
+ final String[] partialNames = selectorsToPartialNames.get(key);
+ if(partialNames == null) {
+ response.sendError(HttpServletResponse.SC_BAD_REQUEST, "No partial names defined for selector " + key);
+ return;
+ }
+ if(log.isDebugEnabled()) {
+ log.debug("Selector {} maps to partial names {}", key, Arrays.asList(partialNames));
+ }
+ aggregator.aggregate(response.getWriter(), partialNames);
+ }
+}
diff --git a/src/test/java/org/apache/sling/graphql/schema/aggregator/LogCapture.java b/src/test/java/org/apache/sling/graphql/schema/aggregator/LogCapture.java
new file mode 100644
index 0000000..312e8a9
--- /dev/null
+++ b/src/test/java/org/apache/sling/graphql/schema/aggregator/LogCapture.java
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ */
+package org.apache.sling.graphql.schema.aggregator;
+
+import static org.junit.Assert.fail;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
+import org.slf4j.LoggerFactory;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.read.ListAppender;
+
+/** Capture logs for testing */
+public class LogCapture extends ListAppender<ILoggingEvent> implements Closeable {
+ private final boolean verboseFailure;
+
+ /** Setup the capture and start it */
+ public LogCapture(String loggerName, boolean verboseFailure) {
+ this.verboseFailure = verboseFailure;
+ Logger logger = (Logger) LoggerFactory.getLogger(loggerName);
+ logger.setLevel(Level.ALL);
+ setContext((LoggerContext) LoggerFactory.getILoggerFactory());
+ logger.addAppender(this);
+ start();
+ }
+
+ public boolean anyMatch(Predicate<ILoggingEvent> p) {
+ return this.list.stream().anyMatch(p);
+ }
+
+ public void assertContains(Level atLevel, String ... substrings) {
+ Stream.of(substrings).forEach(substring -> {
+ if(!anyMatch(event -> event.getLevel() == atLevel && event.getFormattedMessage().contains(substring))) {
+ if(verboseFailure) {
+ fail(String.format("No log message contains [%s] in log\n%s", substring, this.list.toString()));
+ } else {
+ fail(String.format("No log message contains [%s]", substring));
+ }
+ }
+ });
+ }
+
+ @Override
+ public void close() throws IOException {
+ stop();
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/graphql/schema/aggregator/U.java b/src/test/java/org/apache/sling/graphql/schema/aggregator/U.java
new file mode 100644
index 0000000..72fea60
--- /dev/null
+++ b/src/test/java/org/apache/sling/graphql/schema/aggregator/U.java
@@ -0,0 +1,134 @@
+/*
+ * 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.
+ */
+package org.apache.sling.graphql.schema.aggregator;
+
+import org.osgi.framework.Bundle;
+
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.ops4j.pax.tinybundles.core.TinyBundles.bundle;
+
+import org.apache.sling.graphql.schema.aggregator.impl.ProviderBundleTracker;
+import org.ops4j.pax.exam.Option;
+import org.ops4j.pax.tinybundles.core.TinyBundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.wiring.BundleRevision;
+import org.osgi.framework.wiring.BundleWire;
+import org.osgi.framework.wiring.BundleWiring;
+import org.osgi.namespace.extender.ExtenderNamespace;
+
+import static org.ops4j.pax.exam.CoreOptions.streamBundle;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.List;
+
+/** Test Utilities */
+public class U {
+
+ public static Bundle mockProviderBundle(BundleContext bc, String symbolicName, long id, String ... schemaNames) throws IOException {
+ final Bundle b = mock(Bundle.class);
+ final BundleWiring wiring = mock(BundleWiring.class);
+ when(b.getSymbolicName()).thenReturn(symbolicName);
+ when(b.getBundleId()).thenReturn(id);
+ when(b.adapt(BundleWiring.class)).thenReturn(wiring);
+ final BundleWire wire = mock(BundleWire.class);
+ when(wiring.getRequiredWires(ExtenderNamespace.EXTENDER_NAMESPACE)).thenReturn(Collections.singletonList(wire));
+ final BundleRevision revision = mock(BundleRevision.class);
+ when(wire.getProvider()).thenReturn(revision);
+ when(revision.getBundle()).thenAnswer(invocationOnMock -> bc.getBundle());
+
+ final Dictionary<String, String> headers = new Hashtable<>();
+ String fakePath = symbolicName + "/path/" + id;
+ headers.put(ProviderBundleTracker.SCHEMA_PATH_HEADER, fakePath);
+ when(b.getHeaders()).thenReturn(headers);
+
+ final List<String> resources = new ArrayList<>();
+ for(String name : schemaNames) {
+ URL partial = testFileURL(name);
+ if(partial == null) {
+ partial = fakePartialURL(name);
+ }
+ String fakeResource = fakePath + "/resource/" + name;
+ resources.add(fakeResource);
+ when(b.getEntry(fakeResource)).thenReturn(partial);
+ }
+ when(b.getEntryPaths(fakePath)).thenReturn(Collections.enumeration(resources));
+ return b;
+ }
+
+ /** Simple way to get a URL: create a temp file */
+ public static URL fakePartialURL(String name) throws IOException {
+ final File f = File.createTempFile(name, "txt");
+ f.deleteOnExit();
+ final PrintWriter w = new PrintWriter(new FileWriter(f));
+ w.print(fakePartialSchema(name));
+ w.flush();
+ w.close();
+ // Safe in our case, we're using acceptable characters in the path
+ return f.toURL();
+ }
+
+ public static URL testFileURL(String name) {
+ return U.class.getResource(String.format("/partials/%s", name));
+ }
+
+ public static String fakePartialSchema(String name) {
+ return String.format("PARTIAL:%s\nQUERY:%s\nFake query for %s\n", name, name, name);
+ }
+
+ public static Option tinyProviderBundle(String symbolicName, String ... partialsNames) {
+ final String schemaPath = symbolicName + "/schemas";
+ final TinyBundle b = bundle()
+ .set(ProviderBundleTracker.SCHEMA_PATH_HEADER, schemaPath)
+ .set(Constants.BUNDLE_SYMBOLICNAME, symbolicName)
+ .set(
+ Constants.REQUIRE_CAPABILITY,
+ "osgi.extender;filter:=\"(&(osgi.extender=sling.graphql-schema-aggregator)(version>=0.1)(!(version>=1.0)))\""
+ )
+ ;
+
+ for(String name : partialsNames) {
+ final String resourcePath = schemaPath + "/" + name + ".txt";
+ b.add(resourcePath, new ByteArrayInputStream(fakePartialSchema(name).getBytes()));
+ }
+
+ return streamBundle(b.build());
+ }
+
+ public static void assertPartialsFoundInSchema(String output, String ... partialName) {
+ for(String name : partialName) {
+ final String expected = "DefaultSchemaAggregator.source=" + name;
+ if(!output.contains(expected)) {
+ fail(String.format("Expecting output to contain %s: %s", expected, output));
+ }
+ }
+ }
+}
diff --git a/src/test/java/org/apache/sling/graphql/schema/aggregator/impl/BundleEntryPartialProviderTest.java b/src/test/java/org/apache/sling/graphql/schema/aggregator/impl/BundleEntryPartialProviderTest.java
new file mode 100644
index 0000000..428ac1b
--- /dev/null
+++ b/src/test/java/org/apache/sling/graphql/schema/aggregator/impl/BundleEntryPartialProviderTest.java
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+package org.apache.sling.graphql.schema.aggregator.impl;
+
+import static org.junit.Assert.assertEquals;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import org.junit.Test;
+
+public class BundleEntryPartialProviderTest {
+ @Test
+ public void partialName() throws MalformedURLException {
+ final URL url = new URL("http://stuff/some/path/the.name.txt");
+ assertEquals("the.name", BundleEntryPartialProvider.getPartialName(url));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/graphql/schema/aggregator/impl/CapitalizeTest.java b/src/test/java/org/apache/sling/graphql/schema/aggregator/impl/CapitalizeTest.java
new file mode 100644
index 0000000..aaca60d
--- /dev/null
+++ b/src/test/java/org/apache/sling/graphql/schema/aggregator/impl/CapitalizeTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+package org.apache.sling.graphql.schema.aggregator.impl;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+public class CapitalizeTest {
+ @Test
+ public void normalStrings() throws Exception {
+ assertEquals("Voici", DefaultSchemaAggregator.capitalize("voici"));
+ assertEquals("Ou bien", DefaultSchemaAggregator.capitalize("OU BIEN"));
+ }
+
+ @Test
+ public void emptyStrings() throws Exception {
+ assertEquals("", DefaultSchemaAggregator.capitalize(""));
+ assertEquals(null, DefaultSchemaAggregator.capitalize(null));
+ }
+
+ @Test
+ public void shortStrings() throws Exception {
+ assertEquals("A", DefaultSchemaAggregator.capitalize("a"));
+ assertEquals("B", DefaultSchemaAggregator.capitalize("B"));
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..2726ccf
--- /dev/null
+++ b/src/test/java/org/apache/sling/graphql/schema/aggregator/impl/DefaultSchemaAggregatorTest.java
@@ -0,0 +1,175 @@
+/*
+ * 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.
+ */
+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;
+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;
+import org.junit.Test;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class DefaultSchemaAggregatorTest {
+ private DefaultSchemaAggregator dsa;
+ private ProviderBundleTracker tracker;
+ private BundleContext bundleContext;
+
+ @Before
+ public void setup() throws Exception {
+ dsa = new DefaultSchemaAggregator();
+ final Field f = dsa.getClass().getDeclaredField("tracker");
+ f.setAccessible(true);
+ bundleContext = mock(BundleContext.class);
+ when(bundleContext.getBundle()).thenReturn(mock(Bundle.class));
+ tracker = new ProviderBundleTracker();
+ tracker.activate(bundleContext);
+ f.set(dsa, tracker);
+ }
+
+ private void assertContainsIgnoreCase(String substring, String source) {
+ assertTrue("Expecting '" + substring + "' in source string ", source.toLowerCase().contains(substring.toLowerCase()));
+ }
+
+ @Test
+ public void noProviders() throws Exception{
+ final StringWriter target = new StringWriter();
+ final IOException iox = assertThrows(IOException.class, () -> dsa.aggregate(target, "Aprov", "Bprov"));
+ assertContainsIgnoreCase("missing providers", iox.getMessage());
+ assertContainsIgnoreCase("Aprov", iox.getMessage());
+ assertContainsIgnoreCase("Bprov", iox.getMessage());
+ assertContainsIgnoreCase("schema aggregated by DefaultSchemaAggregator", target.toString());
+ }
+
+ @Test
+ public void severalProviders() throws Exception{
+ final StringWriter target = new StringWriter();
+ tracker.addingBundle(U.mockProviderBundle(bundleContext, "A", 1, "1.txt", "2.z.w", "3abc", "4abc"), null);
+ tracker.addingBundle(U.mockProviderBundle(bundleContext, "B", 2, "B1a.txt", "B2.xy"), null);
+ dsa.aggregate(target, "B1a", "B2", "2.z");
+ final String sdl = target.toString().trim();
+ assertContainsIgnoreCase("schema aggregated by DefaultSchemaAggregator", sdl);
+
+ try(InputStream is = getClass().getResourceAsStream("/several-providers-output.txt")) {
+ assertNotNull("Expecting test resource to be present", is);
+ final String expected = IOUtils.toString(is, "UTF-8");
+ assertEquals(expected, sdl);
+ }
+ }
+
+ @Test
+ public void regexpSelection() throws Exception {
+ final StringWriter target = new StringWriter();
+ tracker.addingBundle(U.mockProviderBundle(bundleContext, "A", 1, "a.authoring.1.txt", "a.authoring.2.txt", "3.txt", "4.txt"), null);
+ tracker.addingBundle(U.mockProviderBundle(bundleContext, "B", 2, "B1.txt", "B.authoring.txt"), null);
+ dsa.aggregate(target, "B1", "/.*\\.authoring.*/");
+ assertContainsIgnoreCase("schema aggregated by DefaultSchemaAggregator", target.toString());
+ U.assertPartialsFoundInSchema(target.toString(), "a.authoring.1", "a.authoring.2", "B.authoring", "B1");
+ }
+
+ @Test
+ public void parseResult() throws Exception {
+ final StringWriter target = new StringWriter();
+ tracker.addingBundle(U.mockProviderBundle(bundleContext, "SDL", 1, "a.sdl.txt", "b.sdl.txt", "c.sdl.txt"), null);
+
+ dsa.aggregate(target, "/.*/");
+
+ // Parse the output with a real SDL parser
+ final String sdl = target.toString();
+ final TypeDefinitionRegistry reg = new SchemaParser().parse(sdl);
+
+ // And make sure it contains what we expect
+ assertTrue(reg.getDirectiveDefinition("fetcher").isPresent());
+ assertTrue(reg.getType("SlingResourceConnection").isPresent());
+ assertTrue(reg.getType("PageInfo").isPresent());
+
+ final Optional<TypeDefinition> query = reg.getType("Query");
+ assertTrue("Expecting Query", query.isPresent());
+ assertTrue(query.get().getChildren().toString().contains("oneSchemaResource"));
+ assertTrue(query.get().getChildren().toString().contains("oneSchemaQuery"));
+
+ final Optional<TypeDefinition> mutation = reg.getType("Mutation");
+ assertTrue("Expecting Mutation", mutation.isPresent());
+ assertTrue(mutation.get().getChildren().toString().contains("someMutation"));
+ }
+
+ @Test
+ public void requires() throws Exception {
+ final StringWriter target = new StringWriter();
+ tracker.addingBundle(U.mockProviderBundle(bundleContext, "SDL", 1, "a.sdl.txt", "b.sdl.txt", "c.sdl.txt"), null);
+ dsa.aggregate(target, "c.sdl");
+ final String sdl = target.toString();
+
+ // Verify that required partials are included
+ Stream.of(
+ "someMutation",
+ "typeFromB",
+ "typeFromA"
+ ).forEach((s -> {
+ assertTrue("Expecting aggregate to contain " + s, sdl.contains(s));
+ }));
+ }
+
+ @Test
+ public void cycleInRequirements() throws Exception {
+ final StringWriter target = new StringWriter();
+ tracker.addingBundle(U.mockProviderBundle(bundleContext, "SDL", 1, "circularA.txt", "circularB.txt"), null);
+ final RuntimeException rex = assertThrows(RuntimeException.class, () -> dsa.aggregate(target, "circularA"));
+
+ Stream.of(
+ "requirements cycle",
+ "circularA"
+ ).forEach((s -> {
+ assertTrue(String.format("Expecting message to contain %s: %s", s, rex.getMessage()), rex.getMessage().contains(s));
+ }));
+ }
+
+ @Test
+ public void providersOrdering() throws Exception {
+ final StringWriter target = new StringWriter();
+ tracker.addingBundle(U.mockProviderBundle(bundleContext, "ordering", 1, "Aprov.txt", "Cprov.txt", "Z_test.txt", "A_test.txt",
+ "Zprov.txt",
+ "Z_test.txt", "Bprov.txt", "C_test.txt"), null);
+ dsa.aggregate(target, "Aprov", "Zprov", "/[A-Z]_test/", "A_test", "Cprov");
+ final String sdl = target.toString();
+
+ // The order of named partials is kept, regexp selected ones are ordered by name
+ // And A_test has already been used so it's not used again when called explicitly after regexp
+ final String expected = "End of Schema aggregated from {Aprov,Zprov,A_test,C_test,Z_test,Cprov} by DefaultSchemaAggregator";
+ assertTrue(String.format("Expecting schema to contain [%s]: %s", expected, sdl), sdl.contains(expected));
+ }
+}
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
new file mode 100644
index 0000000..737f9e1
--- /dev/null
+++ b/src/test/java/org/apache/sling/graphql/schema/aggregator/impl/PartialReaderTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.
+ */
+package org.apache.sling.graphql.schema.aggregator.impl;
+
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.UnsupportedEncodingException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+
+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.junit.Assert.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Optional;
+import java.util.function.Supplier;
+import java.util.regex.Pattern;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.sling.graphql.schema.aggregator.U;
+import org.junit.Test;
+
+public class PartialReaderTest {
+ public static final String CHARSET = "UTF-8";
+ private static final String NONAME = "<NO NAME>";
+
+ private void assertSection(Partial p, String name, String description, String contentRegexp) throws IOException {
+ final Optional<Partial.Section> os = p.getSection(name);
+ assertTrue("Expecting section " + name, os.isPresent());
+ final Partial.Section s = os.get();
+ if(description != null) {
+ assertEquals("For section " + name, description, s.getDescription());
+ }
+ if(contentRegexp != null) {
+ try(Reader r = s.getContent()) {
+ final String actual = IOUtils.toString(s.getContent()).trim();
+ final Pattern regexp = Pattern.compile(contentRegexp, Pattern.DOTALL);
+ assertTrue(
+ String.format("Expecting section %s to match %s but was [%s]", name, contentRegexp, actual),
+ regexp.matcher(actual).matches()
+ );
+ }
+ }
+ }
+
+ private Supplier<Reader> getResourceReaderSupplier(String resourceName) {
+ return () -> {
+ try {
+ final InputStream input = getClass().getResourceAsStream(resourceName);
+ assertNotNull("Expecting resource " + resourceName, input);
+ return new InputStreamReader(input, CHARSET);
+ } catch(UnsupportedEncodingException uee) {
+ throw new RuntimeException("Unsupported encoding " + CHARSET, uee);
+ }
+ };
+ }
+
+ private Supplier<Reader> getStringReaderSupplier(String content) {
+ return () -> new StringReader(content);
+ }
+
+ @Test
+ public void parseExample() throws Exception {
+ final PartialReader p = new PartialReader(NONAME, getResourceReaderSupplier("/partials/example.partial.txt"));
+ assertSection(p, "PARTIAL", "Example GraphQL schema partial", "The contents.*PARTIAL.*PARTIAL.*PARTIAL.*equired section\\.");
+ assertSection(p, "REQUIRE", "base.scalars, base.schema", null);
+ assertSection(p, "PROLOGUE", "", "The prologue content.*the aggregated schema.*other sections\\.");
+ assertSection(p, "QUERY", "", "The optional query sections of all partials are aggregated in a query \\{\\} section in the output\\.");
+ assertSection(p, "MUTATION", "", "The optional mutation sections of all partials are aggregated in a mutation \\{\\} section in the output\\.");
+ assertSection(p, "TYPES", "", "The types sections.*mutation(\\s)+sections\\.");
+ }
+
+ @Test
+ public void accentedCharacters() throws Exception {
+ final PartialReader p = new PartialReader(NONAME, getResourceReaderSupplier("/partials/utf8.partial.txt"));
+ assertSection(p, "PARTIAL",
+ "Example GraphQL schema partial with caract\u00E8res accentu\u00E9s",
+ "L'\u00E9t\u00E9 nous \u00E9vitons l'\u00E2tre et pr\u00E9f\u00E9rons Chateaun\u00F6f et les \u00E4kr\u00E0s."
+ );
+ }
+
+ @Test
+ public void missingPartialSection() throws Exception {
+ final Exception e = assertThrows(
+ PartialReader.SyntaxException.class,
+ () -> new PartialReader(NONAME, getStringReaderSupplier(""))
+ );
+ final String expected = "Missing required PARTIAL section";
+ assertTrue(String.format("Expected %s in %s", expected, e.getMessage()), e.getMessage().contains(expected));
+ }
+
+ @Test
+ public void duplicateSection() throws Exception {
+ final Exception e = assertThrows(
+ PartialReader.SyntaxException.class,
+ () -> new PartialReader(NONAME, getResourceReaderSupplier("/partials/duplicate.section.partial.txt"))
+ );
+ final String expected = "Duplicate section DUPLICATE";
+ assertTrue(String.format("Expected %s in %s", expected, e.getMessage()), e.getMessage().contains(expected));
+ }
+
+ @Test
+ public void requires() throws Exception {
+ final PartialReader p = new PartialReader(NONAME, getResourceReaderSupplier("/partials/c.sdl.txt"));
+ assertTrue("Expecting requires section", p.getSection(PartialConstants.S_REQUIRES).isPresent());
+ assertEquals("[a.sdl, b.sdl]", p.getRequiredPartialNames().toString());
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..7a82e7a
--- /dev/null
+++ b/src/test/java/org/apache/sling/graphql/schema/aggregator/impl/ProviderBundleTrackerTest.java
@@ -0,0 +1,103 @@
+/*
+ * 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.
+ */
+package org.apache.sling.graphql.schema.aggregator.impl;
+
+import java.io.IOException;
+import java.util.Optional;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.sling.graphql.schema.aggregator.LogCapture;
+import org.apache.sling.graphql.schema.aggregator.U;
+import org.junit.Before;
+import org.junit.Test;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+
+import ch.qos.logback.classic.Level;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ProviderBundleTrackerTest {
+ private ProviderBundleTracker tracker;
+ private static long bundleId;
+ private BundleContext bundleContext;
+
+ @Before
+ public void setup() {
+ bundleId = 0;
+ bundleContext = mock(BundleContext.class);
+ when(bundleContext.getBundle()).thenReturn(mock(Bundle.class));
+ tracker = new ProviderBundleTracker();
+ tracker.activate(bundleContext);
+ }
+
+ @Test
+ public void addBundle() throws Exception {
+ final Bundle a = U.mockProviderBundle(bundleContext, "A", ++bundleId, "1.txt");
+ tracker.addingBundle(a, null);
+ assertEquals(1, tracker.getSchemaProviders().size());
+
+ final Partial s = tracker.getSchemaProviders().values().iterator().next();
+ assertTrue(s.toString().contains(a.getSymbolicName()));
+ assertTrue(s.toString().contains("1.txt"));
+ }
+
+ @Test
+ public void addAndRemoveBundles() throws Exception {
+ final Bundle a = U.mockProviderBundle(bundleContext, "A", ++bundleId, "1.graphql.txt");
+ final Bundle b = U.mockProviderBundle(bundleContext, "B", ++bundleId, "2.txt", "1.txt");
+ tracker.addingBundle(a, null);
+ tracker.addingBundle(b, null);
+ assertEquals(3, tracker.getSchemaProviders().size());
+ tracker.removedBundle(b, null, null);
+ assertEquals(1, tracker.getSchemaProviders().size());
+ tracker.removedBundle(a, null, null);
+ assertEquals(0, tracker.getSchemaProviders().size());
+ tracker.removedBundle(a, null, null);
+ assertEquals(0, tracker.getSchemaProviders().size());
+ }
+
+ @Test
+ public void duplicatePartialName() throws Exception {
+ final LogCapture capture = new LogCapture(ProviderBundleTracker.class.getName(), true);
+ final Bundle a = U.mockProviderBundle(bundleContext, "A", ++bundleId, "TT.txt");
+ 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");
+ assertEquals(2, tracker.getSchemaProviders().size());
+ }
+
+ private void assertSectionContent(Partial p, String name, String expected) throws IOException {
+ final Optional<Partial.Section> os = p.getSection(name);
+ assertTrue("Expecting section " + name, os.isPresent());
+ assertEquals(expected, IOUtils.toString(os.get().getContent()).trim());
+ }
+
+ @Test
+ public void getSectionsContent() throws IOException {
+ final Bundle a = U.mockProviderBundle(bundleContext, "A", ++bundleId, "1.txt");
+ tracker.addingBundle(a, null);
+ final Partial p = tracker.getSchemaProviders().values().iterator().next();
+ assertSectionContent(p, PartialConstants.S_QUERY, "Fake query for 1.txt");
+ }
+}
diff --git a/src/test/java/org/apache/sling/graphql/schema/aggregator/impl/SchemaAggregatorServletTest.java b/src/test/java/org/apache/sling/graphql/schema/aggregator/impl/SchemaAggregatorServletTest.java
new file mode 100644
index 0000000..aa801e6
--- /dev/null
+++ b/src/test/java/org/apache/sling/graphql/schema/aggregator/impl/SchemaAggregatorServletTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+package org.apache.sling.graphql.schema.aggregator.impl;
+
+import org.apache.sling.graphql.schema.aggregator.servlet.SchemaAggregatorServlet;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.lang.reflect.Field;
+import java.util.Map;
+
+public class SchemaAggregatorServletTest {
+
+ private void assertMappings(Map<String, String[]> data, String selector, String expected) {
+ final String [] names = data.get(selector);
+ assertNotNull("Expecting field names for selector " + selector, names);
+ assertEquals(expected, String.join(",", names));
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void selectorMappingConfig() throws Exception {
+ final SchemaAggregatorServlet s = new SchemaAggregatorServlet();
+ final SchemaAggregatorServlet.Config cfg = mock(SchemaAggregatorServlet.Config.class);
+ final String [] cfgMappings = {
+ "\t S1\t :one, two, \t three \t",
+ "selector_2:4,5"
+ };
+ when(cfg.selectors_to_partials_mapping()).thenReturn(cfgMappings);
+ s.activate(null, cfg);
+ final Field f = s.getClass().getDeclaredField("selectorsToPartialNames");
+ f.setAccessible(true);
+ final Map<String, String[]> actualMappings = (Map<String, String[]>)f.get(s);
+ assertEquals(2, actualMappings.size());
+ assertMappings(actualMappings, "S1", "one,two,three");
+ assertMappings(actualMappings, "selector_2", "4,5");
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/graphql/schema/aggregator/it/SchemaAggregatorServletIT.java b/src/test/java/org/apache/sling/graphql/schema/aggregator/it/SchemaAggregatorServletIT.java
new file mode 100644
index 0000000..e3fb2e3
--- /dev/null
+++ b/src/test/java/org/apache/sling/graphql/schema/aggregator/it/SchemaAggregatorServletIT.java
@@ -0,0 +1,74 @@
+/*
+ * 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.
+ */
+package org.apache.sling.graphql.schema.aggregator.it;
+
+import org.ops4j.pax.exam.Configuration;
+import org.ops4j.pax.exam.Option;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerClass;
+
+import static org.ops4j.pax.exam.cm.ConfigurationAdminOptions.factoryConfiguration;
+
+import org.apache.sling.graphql.schema.aggregator.U;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerClass.class)
+public class SchemaAggregatorServletIT extends SchemaAggregatorTestSupport {
+ private static final String AGGREGATOR_SERVLET_CONFIG_PID = "org.apache.sling.graphql.schema.aggregator.SchemaAggregatorServlet";
+ private static final String GQL_SCHEMA_EXT = "GQLschema";
+
+ @Configuration
+ public Option[] configuration() {
+ return new Option[]{
+ baseConfiguration(),
+
+ U.tinyProviderBundle("firstProvider", "firstA", "firstB","secondN"),
+ U.tinyProviderBundle("secondProvider", "secondA", "secondB","secondOther"),
+
+ // Configure the org.apache.sling.graphql.schema.aggregator.SchemaAggregatorServlet
+ factoryConfiguration(AGGREGATOR_SERVLET_CONFIG_PID)
+ .put("sling.servlet.resourceTypes", "sling/servlet/default")
+ // The extension must be the one used by the GraphQLServlet to retrieve schemas
+ .put("sling.servlet.extensions", GQL_SCHEMA_EXT)
+ // The GraphQLServlet uses an internal GET request for the schema
+ .put("sling.servlet.methods", new String[] { "GET" })
+ // Several selectors can be configured to setup API planes, each with their own GraphQL schema
+ .put("sling.servlet.selectors", new String[] { "X", "Y", "nomappings" })
+ // This mapping defines which partials to use to build the schema for each selector
+ // The lists can use either the exact names of partials, or (Java flavored) regular expressions on
+ // their names, identified by a starting an ending slash.
+ .put("selectors.to.partials.mapping", new String[] { "X:firstA,secondB", "Y:secondA,firstB,/second.*/" })
+ .asOption(),
+ };
+ }
+
+ @Test
+ public void basicAggregation() throws Exception {
+ U.assertPartialsFoundInSchema(getContent("/.X." + GQL_SCHEMA_EXT), "firstA", "secondB");
+ U.assertPartialsFoundInSchema(getContent("/.Y." + GQL_SCHEMA_EXT), "secondA", "firstB", "secondB","secondOther","secondN");
+ }
+
+ @Test
+ public void unmappedSelector() throws Exception {
+ executeRequest("GET", "/.nomappings." + GQL_SCHEMA_EXT, null, null, null, 400);
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..8163c41
--- /dev/null
+++ b/src/test/java/org/apache/sling/graphql/schema/aggregator/it/SchemaAggregatorTestSupport.java
@@ -0,0 +1,150 @@
+/*
+ * 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.
+ */
+package org.apache.sling.graphql.schema.aggregator.it;
+
+import java.io.Reader;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import javax.inject.Inject;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.engine.SlingRequestProcessor;
+import org.apache.sling.servlethelpers.MockSlingHttpServletResponse;
+import org.apache.sling.servlethelpers.internalrequests.SlingInternalRequest;
+import org.apache.sling.testing.paxexam.TestSupport;
+import org.junit.Before;
+import org.ops4j.pax.exam.Option;
+import org.ops4j.pax.exam.options.ModifiableCompositeOption;
+import org.ops4j.pax.exam.options.extra.VMOption;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.apache.sling.testing.paxexam.SlingOptions.slingQuickstartOakTar;
+import static org.apache.sling.testing.paxexam.SlingOptions.slingScripting;
+import static org.apache.sling.testing.paxexam.SlingOptions.slingScriptingJsp;
+import static org.junit.Assert.fail;
+import static org.ops4j.pax.exam.CoreOptions.composite;
+import static org.ops4j.pax.exam.CoreOptions.junitBundles;
+import static org.ops4j.pax.exam.CoreOptions.mavenBundle;
+import static org.ops4j.pax.exam.CoreOptions.when;
+import static org.ops4j.pax.exam.cm.ConfigurationAdminOptions.newConfiguration;
+
+public abstract class SchemaAggregatorTestSupport extends TestSupport {
+
+ private final Logger log = LoggerFactory.getLogger(getClass().getName());
+ private final static int STARTUP_WAIT_SECONDS = 30;
+
+ @Inject
+ protected ResourceResolverFactory resourceResolverFactory;
+
+ @Inject
+ protected SlingRequestProcessor requestProcessor;
+
+ protected ModifiableCompositeOption baseConfiguration() {
+ final String vmOpt = System.getProperty("pax.vm.options");
+ VMOption vmOption = null;
+ if (StringUtils.isNotEmpty(vmOpt)) {
+ vmOption = new VMOption(vmOpt);
+ }
+
+ final String jacocoOpt = System.getProperty("jacoco.command");
+ VMOption jacocoCommand = null;
+ if (StringUtils.isNotEmpty(jacocoOpt)) {
+ jacocoCommand = new VMOption(jacocoOpt);
+ }
+
+ return composite(
+ when(vmOption != null).useOptions(vmOption),
+ when(jacocoCommand != null).useOptions(jacocoCommand),
+ super.baseConfiguration(),
+ slingQuickstart(),
+ testBundle("bundle.filename"),
+ newConfiguration("org.apache.sling.jcr.base.internal.LoginAdminWhitelist")
+ .put("whitelist.bundles.regexp", "^PAXEXAM.*$")
+ .asOption(),
+ mavenBundle().groupId("org.apache.sling").artifactId("org.apache.sling.servlet-helpers").versionAsInProject(),
+ junitBundles()
+ );
+ }
+
+ private Option slingQuickstart() {
+ final int httpPort = findFreePort();
+ log.info("Using HTTP port {}", httpPort);
+ final String workingDirectory = workingDirectory();
+ return composite(
+ slingQuickstartOakTar(workingDirectory, httpPort),
+ slingScripting(),
+ slingScriptingJsp()
+ );
+ }
+
+ /**
+ * Injecting the appropriate services to wait for would be more elegant but this is very reliable..
+ */
+ @Before
+ public void waitForSling() throws Exception {
+ final int expectedStatus = 200;
+ final List<Integer> statuses = new ArrayList<>();
+ final String path = "/.json";
+ final Instant endTime = Instant.now().plus(Duration.ofSeconds(STARTUP_WAIT_SECONDS));
+
+ while(Instant.now().isBefore(endTime)) {
+ final int status = executeRequest("GET", path, null, null, null, -1).getStatus();
+ statuses.add(status);
+ if (status == expectedStatus) {
+ return;
+ }
+ Thread.sleep(250);
+ }
+
+ fail("Did not get a " + expectedStatus + " status at " + path + " got " + statuses);
+ }
+
+ protected MockSlingHttpServletResponse executeRequest(final String method,
+ final String path, Map<String, Object> params, String contentType,
+ Reader body, final int expectedStatus) throws Exception {
+
+ // Admin resolver is fine for testing
+ @SuppressWarnings("deprecation")
+ final ResourceResolver resourceResolver = resourceResolverFactory.getAdministrativeResourceResolver(null);
+
+ final int [] statusParam = expectedStatus == -1 ? null : new int[] { expectedStatus };
+
+ return (MockSlingHttpServletResponse)
+ new SlingInternalRequest(resourceResolver, requestProcessor, path)
+ .withRequestMethod(method)
+ .withParameters(params)
+ .withContentType(contentType)
+ .withBody(body)
+ .execute()
+ .checkStatus(statusParam)
+ .getResponse()
+ ;
+ }
+
+ 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/logback.xml b/src/test/resources/logback.xml
new file mode 100644
index 0000000..254317a
--- /dev/null
+++ b/src/test/resources/logback.xml
@@ -0,0 +1,31 @@
+<!--
+ 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.
+-->
+<configuration>
+ <appender name="file" class="ch.qos.logback.core.FileAppender">
+ <file>target/test.log</file>
+ <append>true</append>
+ <encoder>
+ <pattern>%date level=%level thread=%thread logger=%logger sourcefile=%file line=%line %mdc message=%msg%n</pattern>
+ </encoder>
+ </appender>
+
+ <root level="INFO">
+ <appender-ref ref="file" />
+ </root>
+
+ <logger name="org.apache.sling.graphql" level="DEBUG"/>
+</configuration>
\ No newline at end of file
diff --git a/src/test/resources/partials/a.sdl.txt b/src/test/resources/partials/a.sdl.txt
new file mode 100644
index 0000000..55bb978
--- /dev/null
+++ b/src/test/resources/partials/a.sdl.txt
@@ -0,0 +1,21 @@
+PARTIAL: Test A, SDL
+
+PROLOGUE:
+directive @fetcher(
+ name : String,
+ options : String = "",
+ source : String = ""
+) on FIELD_DEFINITION
+
+TYPES:
+type SlingResource {
+ path: String!
+}
+
+type PageInfo {
+ count: Int
+}
+
+type typeFromA {
+ path : String
+}
\ No newline at end of file
diff --git a/src/test/resources/partials/b.sdl.txt b/src/test/resources/partials/b.sdl.txt
new file mode 100644
index 0000000..4e4c2bb
--- /dev/null
+++ b/src/test/resources/partials/b.sdl.txt
@@ -0,0 +1,10 @@
+PARTIAL: Test B, SDL
+
+TYPES:
+type SlingResourceConnection {
+ pageInfo : PageInfo
+}
+
+type typeFromB {
+ path : String
+}
\ No newline at end of file
diff --git a/src/test/resources/partials/c.sdl.txt b/src/test/resources/partials/c.sdl.txt
new file mode 100644
index 0000000..a9a4613
--- /dev/null
+++ b/src/test/resources/partials/c.sdl.txt
@@ -0,0 +1,10 @@
+PARTIAL: Test C, SDL
+
+REQUIRES: a.sdl, b.sdl
+
+QUERY:
+oneSchemaResource : SlingResource @fetcher(name:"test/pipe" source:"$")
+oneSchemaQuery : SlingResourceConnection @connection(for: "SlingResource") @fetcher(name:"test/query")
+
+MUTATION:
+someMutation : SlingResource
\ No newline at end of file
diff --git a/src/test/resources/partials/circularA.txt b/src/test/resources/partials/circularA.txt
new file mode 100644
index 0000000..76e1c6f
--- /dev/null
+++ b/src/test/resources/partials/circularA.txt
@@ -0,0 +1,6 @@
+PARTIAL: Test circular requirements
+
+REQUIRES: circularB
+
+TYPES:
+type typeFromCircularA { name: String }
diff --git a/src/test/resources/partials/circularB.txt b/src/test/resources/partials/circularB.txt
new file mode 100644
index 0000000..15f5f90
--- /dev/null
+++ b/src/test/resources/partials/circularB.txt
@@ -0,0 +1,6 @@
+PARTIAL: Test circular requirements
+
+REQUIRES: circularA
+
+TYPES:
+type typeFromCircularB { name: String }
\ No newline at end of file
diff --git a/src/test/resources/partials/duplicate.section.partial.txt b/src/test/resources/partials/duplicate.section.partial.txt
new file mode 100644
index 0000000..f92e311
--- /dev/null
+++ b/src/test/resources/partials/duplicate.section.partial.txt
@@ -0,0 +1,6 @@
+PARTIAL: some description
+
+DUPLICATE: will be provided twice
+
+DUPLICATE: here's the second one
+And the last line, required for correct parsing
\ No newline at end of file
diff --git a/src/test/resources/partials/example.partial.txt b/src/test/resources/partials/example.partial.txt
new file mode 100644
index 0000000..4c76c53
--- /dev/null
+++ b/src/test/resources/partials/example.partial.txt
@@ -0,0 +1,27 @@
+# Example GraphQL partial schema
+# Everything before the PARTIAL section is ignored
+# Such partials are aggregated into a composite GraphQL schema,
+# named "output" below.
+
+PARTIAL: Example GraphQL schema partial
+The contents of the PARTIAL section are not included in the output.
+The text that follows PARTIAL is a description of this partial.
+PARTIAL is the only required section.
+
+REQUIRE: base.scalars, base.schema
+The REQUIRE section indicates partials which are required for this
+one to be valid. Its contents are not included in the output.
+
+PROLOGUE:
+The prologue content of all partials is concatenated in the aggregated schema
+before all other sections.
+
+QUERY:
+The optional query sections of all partials are aggregated in a query {} section in the output.
+
+MUTATION:
+The optional mutation sections of all partials are aggregated in a mutation {} section in the output.
+
+TYPES:
+The types sections of all partials are aggregated in the output, after the QUERY and mutation
+sections.
\ No newline at end of file
diff --git a/src/test/resources/partials/utf8.partial.txt b/src/test/resources/partials/utf8.partial.txt
new file mode 100644
index 0000000..17fabb9
--- /dev/null
+++ b/src/test/resources/partials/utf8.partial.txt
@@ -0,0 +1,2 @@
+PARTIAL: Example GraphQL schema partial with caractères accentués
+L'été nous évitons l'âtre et préférons Chateaunöf et les äkràs.
\ No newline at end of file
diff --git a/src/test/resources/several-providers-output.txt b/src/test/resources/several-providers-output.txt
new file mode 100644
index 0000000..b3fa1f7
--- /dev/null
+++ b/src/test/resources/several-providers-output.txt
@@ -0,0 +1,16 @@
+# Schema aggregated by DefaultSchemaAggregator
+
+type Query {
+
+# DefaultSchemaAggregator.source=B1a
+Fake query for B1a.txt
+
+# DefaultSchemaAggregator.source=B2
+Fake query for B2.xy
+
+# DefaultSchemaAggregator.source=2.z
+Fake query for 2.z.w
+
+}
+
+# End of Schema aggregated from {B1a,B2,2.z} by DefaultSchemaAggregator
\ No newline at end of file