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 <[email protected]> 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 @@ +[](https://sling.apache.org) + + [](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
