diff --git a/graphql-java-servlet/build.gradle b/graphql-java-servlet/build.gradle index af5a0815..4eb22872 100644 --- a/graphql-java-servlet/build.gradle +++ b/graphql-java-servlet/build.gradle @@ -29,7 +29,7 @@ dependencies { compileOnly 'org.osgi:org.osgi.service.metatype.annotations:1.3.0' compileOnly 'org.osgi:org.osgi.annotation:6.0.0' - testCompile 'io.github.graphql-java:graphql-java-annotations:5.2' + testCompile 'io.github.graphql-java:graphql-java-annotations:8.3' // Unit testing testCompile "org.codehaus.groovy:groovy-all:2.4.1" @@ -40,4 +40,4 @@ dependencies { testCompile 'org.springframework:spring-test:4.3.7.RELEASE' testRuntime 'org.springframework:spring-web:4.3.7.RELEASE' testCompile 'com.google.guava:guava:24.1.1-jre' -} \ No newline at end of file +} diff --git a/graphql-java-servlet/src/main/java/graphql/kickstart/servlet/OsgiGraphQLHttpServlet.java b/graphql-java-servlet/src/main/java/graphql/kickstart/servlet/OsgiGraphQLHttpServlet.java index d908c97a..a8c2b94f 100644 --- a/graphql-java-servlet/src/main/java/graphql/kickstart/servlet/OsgiGraphQLHttpServlet.java +++ b/graphql-java-servlet/src/main/java/graphql/kickstart/servlet/OsgiGraphQLHttpServlet.java @@ -1,13 +1,7 @@ package graphql.kickstart.servlet; -import static graphql.schema.GraphQLObjectType.newObject; -import static graphql.schema.GraphQLSchema.newSchema; - -import graphql.Scalars; import graphql.execution.preparsed.NoOpPreparsedDocumentProvider; import graphql.execution.preparsed.PreparsedDocumentProvider; -import graphql.kickstart.execution.GraphQLObjectMapper; -import graphql.kickstart.execution.GraphQLQueryInvoker; import graphql.kickstart.execution.GraphQLRootObjectBuilder; import graphql.kickstart.execution.config.DefaultExecutionStrategyProvider; import graphql.kickstart.execution.config.ExecutionStrategyProvider; @@ -15,14 +9,11 @@ import graphql.kickstart.execution.error.DefaultGraphQLErrorHandler; import graphql.kickstart.execution.error.GraphQLErrorHandler; import graphql.kickstart.execution.instrumentation.NoOpInstrumentationProvider; -import graphql.kickstart.servlet.config.DefaultGraphQLSchemaServletProvider; -import graphql.kickstart.servlet.config.GraphQLSchemaServletProvider; import graphql.kickstart.servlet.context.DefaultGraphQLServletContextBuilder; import graphql.kickstart.servlet.context.GraphQLServletContextBuilder; import graphql.kickstart.servlet.core.DefaultGraphQLRootObjectBuilder; import graphql.kickstart.servlet.core.GraphQLServletListener; import graphql.kickstart.servlet.core.GraphQLServletRootObjectBuilder; -import graphql.kickstart.servlet.input.GraphQLInvocationInputFactory; import graphql.kickstart.servlet.osgi.GraphQLCodeRegistryProvider; import graphql.kickstart.servlet.osgi.GraphQLMutationProvider; import graphql.kickstart.servlet.osgi.GraphQLProvider; @@ -30,17 +21,6 @@ import graphql.kickstart.servlet.osgi.GraphQLSubscriptionProvider; import graphql.kickstart.servlet.osgi.GraphQLTypesProvider; import graphql.schema.GraphQLCodeRegistry; -import graphql.schema.GraphQLFieldDefinition; -import graphql.schema.GraphQLObjectType; -import graphql.schema.GraphQLType; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Deactivate; @@ -57,331 +37,186 @@ @Designate(ocd = OsgiGraphQLHttpServletConfiguration.class, factory = true) public class OsgiGraphQLHttpServlet extends AbstractGraphQLHttpServlet { - private final List queryProviders = new ArrayList<>(); - private final List mutationProviders = new ArrayList<>(); - private final List subscriptionProviders = new ArrayList<>(); - private final List typesProviders = new ArrayList<>(); - - private final GraphQLQueryInvoker queryInvoker; - private final GraphQLInvocationInputFactory invocationInputFactory; - private final GraphQLObjectMapper graphQLObjectMapper; - - private GraphQLServletContextBuilder contextBuilder = new DefaultGraphQLServletContextBuilder(); - private GraphQLServletRootObjectBuilder rootObjectBuilder = new DefaultGraphQLRootObjectBuilder(); - private ExecutionStrategyProvider executionStrategyProvider = new DefaultExecutionStrategyProvider(); - private InstrumentationProvider instrumentationProvider = new NoOpInstrumentationProvider(); - private GraphQLErrorHandler errorHandler = new DefaultGraphQLErrorHandler(); - private PreparsedDocumentProvider preparsedDocumentProvider = NoOpPreparsedDocumentProvider.INSTANCE; - private GraphQLCodeRegistryProvider codeRegistryProvider = () -> GraphQLCodeRegistry - .newCodeRegistry().build(); - - private GraphQLSchemaServletProvider schemaProvider; - - private ScheduledExecutorService executor; - private ScheduledFuture updateFuture; - private int schemaUpdateDelay; + private final OsgiSchemaBuilder schemaBuilder = new OsgiSchemaBuilder(); public OsgiGraphQLHttpServlet() { - updateSchema(); - - this.queryInvoker = GraphQLQueryInvoker.newBuilder() - .withPreparsedDocumentProvider(this::getPreparsedDocumentProvider) - .withInstrumentation(() -> this.getInstrumentationProvider().getInstrumentation()) - .withExecutionStrategyProvider(this::getExecutionStrategyProvider).build(); - - this.invocationInputFactory = GraphQLInvocationInputFactory.newBuilder(this::getSchemaProvider) - .withGraphQLContextBuilder(this::getContextBuilder) - .withGraphQLRootObjectBuilder(this::getRootObjectBuilder) - .build(); - - this.graphQLObjectMapper = GraphQLObjectMapper.newBuilder() - .withGraphQLErrorHandler(this::getErrorHandler) - .build(); + schemaBuilder.updateSchema(); } @Activate public void activate(Config config) { - this.schemaUpdateDelay = config.schema_update_delay(); - if (schemaUpdateDelay != 0) { - executor = Executors.newSingleThreadScheduledExecutor(); - } + schemaBuilder.activate(config.schema_update_delay()); } @Deactivate public void deactivate() { - if (executor != null) { - executor.shutdown(); - } + schemaBuilder.deactivate(); } @Override protected GraphQLConfiguration getConfiguration() { - return GraphQLConfiguration - .with(invocationInputFactory) - .with(queryInvoker) - .with(graphQLObjectMapper) - .build(); + return schemaBuilder.buildConfiguration(); } protected void updateSchema() { - if (schemaUpdateDelay == 0) { - doUpdateSchema(); - } else { - if (updateFuture != null) { - updateFuture.cancel(true); - } - - updateFuture = executor.schedule(this::doUpdateSchema, schemaUpdateDelay, TimeUnit.MILLISECONDS); - } - } - - private void doUpdateSchema() { - final GraphQLObjectType.Builder queryTypeBuilder = newObject().name("Query") - .description("Root query type"); - - if (!queryProviders.isEmpty()) { - for (GraphQLQueryProvider provider : queryProviders) { - if (provider.getQueries() != null && !provider.getQueries().isEmpty()) { - provider.getQueries().forEach(queryTypeBuilder::field); - } - } - } else { - // graphql-java enforces Query type to be there with at least some field. - queryTypeBuilder.field( - GraphQLFieldDefinition - .newFieldDefinition() - .name("_empty") - .type(Scalars.GraphQLBoolean) - .build()); - } - - final Set types = new HashSet<>(); - for (GraphQLTypesProvider typesProvider : typesProviders) { - types.addAll(typesProvider.getTypes()); - } - - GraphQLObjectType mutationType = null; - - if (!mutationProviders.isEmpty()) { - final GraphQLObjectType.Builder mutationTypeBuilder = newObject().name("Mutation") - .description("Root mutation type"); - - for (GraphQLMutationProvider provider : mutationProviders) { - provider.getMutations().forEach(mutationTypeBuilder::field); - } - - if (!mutationTypeBuilder.build().getFieldDefinitions().isEmpty()) { - mutationType = mutationTypeBuilder.build(); - } - } - - GraphQLObjectType subscriptionType = null; - - if (!subscriptionProviders.isEmpty()) { - final GraphQLObjectType.Builder subscriptionTypeBuilder = newObject().name("Subscription") - .description("Root subscription type"); - - for (GraphQLSubscriptionProvider provider : subscriptionProviders) { - provider.getSubscriptions().forEach(subscriptionTypeBuilder::field); - } - - if (!subscriptionTypeBuilder.build().getFieldDefinitions().isEmpty()) { - subscriptionType = subscriptionTypeBuilder.build(); - } - } - - this.schemaProvider = new DefaultGraphQLSchemaServletProvider( - newSchema().query(queryTypeBuilder.build()) - .mutation(mutationType) - .subscription(subscriptionType) - .additionalTypes(types) - .codeRegistry(codeRegistryProvider.getCodeRegistry()) - .build()); + schemaBuilder.updateSchema(); } @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) public void bindProvider(GraphQLProvider provider) { if (provider instanceof GraphQLQueryProvider) { - queryProviders.add((GraphQLQueryProvider) provider); + schemaBuilder.add((GraphQLQueryProvider) provider); } if (provider instanceof GraphQLMutationProvider) { - mutationProviders.add((GraphQLMutationProvider) provider); + schemaBuilder.add((GraphQLMutationProvider) provider); } if (provider instanceof GraphQLSubscriptionProvider) { - subscriptionProviders.add((GraphQLSubscriptionProvider) provider); + schemaBuilder.add((GraphQLSubscriptionProvider) provider); } if (provider instanceof GraphQLTypesProvider) { - typesProviders.add((GraphQLTypesProvider) provider); + schemaBuilder.add((GraphQLTypesProvider) provider); } if (provider instanceof GraphQLCodeRegistryProvider) { - codeRegistryProvider = (GraphQLCodeRegistryProvider) provider; + schemaBuilder.setCodeRegistryProvider((GraphQLCodeRegistryProvider) provider); } updateSchema(); } public void unbindProvider(GraphQLProvider provider) { if (provider instanceof GraphQLQueryProvider) { - queryProviders.remove(provider); + schemaBuilder.remove((GraphQLQueryProvider) provider); } if (provider instanceof GraphQLMutationProvider) { - mutationProviders.remove(provider); + schemaBuilder.remove((GraphQLMutationProvider) provider); } if (provider instanceof GraphQLSubscriptionProvider) { - subscriptionProviders.remove(provider); + schemaBuilder.remove((GraphQLSubscriptionProvider) provider); } if (provider instanceof GraphQLTypesProvider) { - typesProviders.remove(provider); + schemaBuilder.remove((GraphQLTypesProvider) provider); } if (provider instanceof GraphQLCodeRegistryProvider) { - codeRegistryProvider = () -> GraphQLCodeRegistry.newCodeRegistry().build(); + schemaBuilder.setCodeRegistryProvider(() -> GraphQLCodeRegistry.newCodeRegistry().build()); } updateSchema(); } @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) public void bindQueryProvider(GraphQLQueryProvider queryProvider) { - queryProviders.add(queryProvider); + schemaBuilder.add(queryProvider); updateSchema(); } public void unbindQueryProvider(GraphQLQueryProvider queryProvider) { - queryProviders.remove(queryProvider); + schemaBuilder.remove(queryProvider); updateSchema(); } @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) public void bindMutationProvider(GraphQLMutationProvider mutationProvider) { - mutationProviders.add(mutationProvider); + schemaBuilder.add(mutationProvider); updateSchema(); } public void unbindMutationProvider(GraphQLMutationProvider mutationProvider) { - mutationProviders.remove(mutationProvider); + schemaBuilder.remove(mutationProvider); updateSchema(); } @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) public void bindSubscriptionProvider(GraphQLSubscriptionProvider subscriptionProvider) { - subscriptionProviders.add(subscriptionProvider); + schemaBuilder.add(subscriptionProvider); updateSchema(); } public void unbindSubscriptionProvider(GraphQLSubscriptionProvider subscriptionProvider) { - subscriptionProviders.remove(subscriptionProvider); + schemaBuilder.remove(subscriptionProvider); updateSchema(); } @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) public void bindTypesProvider(GraphQLTypesProvider typesProvider) { - typesProviders.add(typesProvider); + schemaBuilder.add(typesProvider); updateSchema(); } public void unbindTypesProvider(GraphQLTypesProvider typesProvider) { - typesProviders.remove(typesProvider); + schemaBuilder.remove(typesProvider); updateSchema(); } @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) public void bindServletListener(GraphQLServletListener listener) { - this.addListener(listener); + schemaBuilder.add(listener); } public void unbindServletListener(GraphQLServletListener listener) { - this.removeListener(listener); + schemaBuilder.remove(listener); } @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC) - public void setContextProvider(GraphQLServletContextBuilder contextBuilder) { - this.contextBuilder = contextBuilder; - } - - public void unsetContextProvider(GraphQLServletContextBuilder contextBuilder) { - this.contextBuilder = new DefaultGraphQLServletContextBuilder(); - } - - public void unsetRootObjectBuilder(GraphQLRootObjectBuilder rootObjectBuilder) { - this.rootObjectBuilder = new DefaultGraphQLRootObjectBuilder(); - } - - public void unsetExecutionStrategyProvider(ExecutionStrategyProvider provider) { - executionStrategyProvider = new DefaultExecutionStrategyProvider(); - } - - public void unsetInstrumentationProvider(InstrumentationProvider provider) { - instrumentationProvider = new NoOpInstrumentationProvider(); - } - - public void unsetErrorHandler(GraphQLErrorHandler errorHandler) { - this.errorHandler = new DefaultGraphQLErrorHandler(); - } - - public void unsetPreparsedDocumentProvider(PreparsedDocumentProvider preparsedDocumentProvider) { - this.preparsedDocumentProvider = NoOpPreparsedDocumentProvider.INSTANCE; + public void setContextBuilder(GraphQLServletContextBuilder contextBuilder) { + schemaBuilder.setContextBuilder(contextBuilder); } - @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY) - public void bindCodeRegistryProvider(GraphQLCodeRegistryProvider graphQLCodeRegistryProvider) { - this.codeRegistryProvider = graphQLCodeRegistryProvider; - updateSchema(); - } - - public void unbindCodeRegistryProvider(GraphQLCodeRegistryProvider graphQLCodeRegistryProvider) { - this.codeRegistryProvider = () -> GraphQLCodeRegistry.newCodeRegistry().build(); - updateSchema(); - } - - public GraphQLServletContextBuilder getContextBuilder() { - return contextBuilder; - } - - public GraphQLServletRootObjectBuilder getRootObjectBuilder() { - return rootObjectBuilder; + public void unsetContextBuilder(GraphQLServletContextBuilder contextBuilder) { + schemaBuilder.setContextBuilder(new DefaultGraphQLServletContextBuilder()); } @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC) public void setRootObjectBuilder(GraphQLServletRootObjectBuilder rootObjectBuilder) { - this.rootObjectBuilder = rootObjectBuilder; + schemaBuilder.setRootObjectBuilder(rootObjectBuilder); } - public ExecutionStrategyProvider getExecutionStrategyProvider() { - return executionStrategyProvider; + public void unsetRootObjectBuilder(GraphQLRootObjectBuilder rootObjectBuilder) { + schemaBuilder.setRootObjectBuilder(new DefaultGraphQLRootObjectBuilder()); } @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY) public void setExecutionStrategyProvider(ExecutionStrategyProvider provider) { - executionStrategyProvider = provider; + schemaBuilder.setExecutionStrategyProvider(provider); } - public InstrumentationProvider getInstrumentationProvider() { - return instrumentationProvider; + public void unsetExecutionStrategyProvider(ExecutionStrategyProvider provider) { + schemaBuilder.setExecutionStrategyProvider(new DefaultExecutionStrategyProvider()); } @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY) public void setInstrumentationProvider(InstrumentationProvider provider) { - instrumentationProvider = provider; + schemaBuilder.setInstrumentationProvider(provider); } - public GraphQLErrorHandler getErrorHandler() { - return errorHandler; + public void unsetInstrumentationProvider(InstrumentationProvider provider) { + schemaBuilder.setInstrumentationProvider(new NoOpInstrumentationProvider()); } @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY) public void setErrorHandler(GraphQLErrorHandler errorHandler) { - this.errorHandler = errorHandler; + schemaBuilder.setErrorHandler(errorHandler); } - public PreparsedDocumentProvider getPreparsedDocumentProvider() { - return preparsedDocumentProvider; + public void unsetErrorHandler(GraphQLErrorHandler errorHandler) { + schemaBuilder.setErrorHandler(new DefaultGraphQLErrorHandler()); } @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY) public void setPreparsedDocumentProvider(PreparsedDocumentProvider preparsedDocumentProvider) { - this.preparsedDocumentProvider = preparsedDocumentProvider; + schemaBuilder.setPreparsedDocumentProvider(preparsedDocumentProvider); } - public GraphQLSchemaServletProvider getSchemaProvider() { - return schemaProvider; + public void unsetPreparsedDocumentProvider(PreparsedDocumentProvider preparsedDocumentProvider) { + schemaBuilder.setPreparsedDocumentProvider(NoOpPreparsedDocumentProvider.INSTANCE); + } + + @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY) + public void bindCodeRegistryProvider(GraphQLCodeRegistryProvider graphQLCodeRegistryProvider) { + schemaBuilder.setCodeRegistryProvider(graphQLCodeRegistryProvider); + updateSchema(); + } + + public void unbindCodeRegistryProvider(GraphQLCodeRegistryProvider graphQLCodeRegistryProvider) { + schemaBuilder.setCodeRegistryProvider(() -> GraphQLCodeRegistry.newCodeRegistry().build()); + updateSchema(); } @interface Config { diff --git a/graphql-java-servlet/src/main/java/graphql/kickstart/servlet/OsgiSchemaBuilder.java b/graphql-java-servlet/src/main/java/graphql/kickstart/servlet/OsgiSchemaBuilder.java new file mode 100644 index 00000000..e7b2c84e --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/kickstart/servlet/OsgiSchemaBuilder.java @@ -0,0 +1,231 @@ +package graphql.kickstart.servlet; + +import static graphql.schema.GraphQLObjectType.newObject; +import static graphql.schema.GraphQLSchema.newSchema; +import static java.util.stream.Collectors.toSet; + +import graphql.Scalars; +import graphql.execution.preparsed.NoOpPreparsedDocumentProvider; +import graphql.execution.preparsed.PreparsedDocumentProvider; +import graphql.kickstart.execution.GraphQLObjectMapper; +import graphql.kickstart.execution.GraphQLQueryInvoker; +import graphql.kickstart.execution.config.DefaultExecutionStrategyProvider; +import graphql.kickstart.execution.config.ExecutionStrategyProvider; +import graphql.kickstart.execution.config.InstrumentationProvider; +import graphql.kickstart.execution.error.DefaultGraphQLErrorHandler; +import graphql.kickstart.execution.error.GraphQLErrorHandler; +import graphql.kickstart.execution.instrumentation.NoOpInstrumentationProvider; +import graphql.kickstart.servlet.config.DefaultGraphQLSchemaServletProvider; +import graphql.kickstart.servlet.config.GraphQLSchemaServletProvider; +import graphql.kickstart.servlet.context.DefaultGraphQLServletContextBuilder; +import graphql.kickstart.servlet.context.GraphQLServletContextBuilder; +import graphql.kickstart.servlet.core.DefaultGraphQLRootObjectBuilder; +import graphql.kickstart.servlet.core.GraphQLServletListener; +import graphql.kickstart.servlet.core.GraphQLServletRootObjectBuilder; +import graphql.kickstart.servlet.input.GraphQLInvocationInputFactory; +import graphql.kickstart.servlet.osgi.GraphQLCodeRegistryProvider; +import graphql.kickstart.servlet.osgi.GraphQLFieldProvider; +import graphql.kickstart.servlet.osgi.GraphQLMutationProvider; +import graphql.kickstart.servlet.osgi.GraphQLQueryProvider; +import graphql.kickstart.servlet.osgi.GraphQLSubscriptionProvider; +import graphql.kickstart.servlet.osgi.GraphQLTypesProvider; +import graphql.schema.GraphQLCodeRegistry; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLType; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import lombok.Setter; + +@Setter +class OsgiSchemaBuilder { + + private final List queryProviders = new ArrayList<>(); + private final List mutationProviders = new ArrayList<>(); + private final List subscriptionProviders = new ArrayList<>(); + private final List typesProviders = new ArrayList<>(); + private final List listeners = new ArrayList<>(); + + private GraphQLServletContextBuilder contextBuilder = new DefaultGraphQLServletContextBuilder(); + private GraphQLServletRootObjectBuilder rootObjectBuilder = new DefaultGraphQLRootObjectBuilder(); + private ExecutionStrategyProvider executionStrategyProvider = new DefaultExecutionStrategyProvider(); + private InstrumentationProvider instrumentationProvider = new NoOpInstrumentationProvider(); + private GraphQLErrorHandler errorHandler = new DefaultGraphQLErrorHandler(); + private PreparsedDocumentProvider preparsedDocumentProvider = NoOpPreparsedDocumentProvider.INSTANCE; + private GraphQLCodeRegistryProvider codeRegistryProvider = () -> GraphQLCodeRegistry + .newCodeRegistry().build(); + + private GraphQLSchemaServletProvider schemaProvider; + + private ScheduledExecutorService executor; + private ScheduledFuture updateFuture; + private int schemaUpdateDelay; + + void activate(int schemaUpdateDelay) { + this.schemaUpdateDelay = schemaUpdateDelay; + if (schemaUpdateDelay != 0) { + executor = Executors.newSingleThreadScheduledExecutor(); + } + } + + void deactivate() { + if (executor != null) { + executor.shutdown(); + } + } + + void updateSchema() { + if (schemaUpdateDelay == 0) { + doUpdateSchema(); + } else { + if (updateFuture != null) { + updateFuture.cancel(true); + } + + updateFuture = executor + .schedule(this::doUpdateSchema, schemaUpdateDelay, TimeUnit.MILLISECONDS); + } + } + + private void doUpdateSchema() { + this.schemaProvider = new DefaultGraphQLSchemaServletProvider( + newSchema().query(buildQueryType()) + .mutation(buildMutationType()) + .subscription(buildSubscriptionType()) + .additionalTypes(buildTypes()) + .codeRegistry(codeRegistryProvider.getCodeRegistry()) + .build()); + } + + private GraphQLObjectType buildQueryType() { + final GraphQLObjectType.Builder queryTypeBuilder = newObject().name("Query") + .description("Root query type"); + + if (!queryProviders.isEmpty()) { + for (GraphQLQueryProvider provider : queryProviders) { + if (provider.getQueries() != null && !provider.getQueries().isEmpty()) { + provider.getQueries().forEach(queryTypeBuilder::field); + } + } + } else { + // graphql-java enforces Query type to be there with at least some field. + queryTypeBuilder.field( + GraphQLFieldDefinition + .newFieldDefinition() + .name("_empty") + .type(Scalars.GraphQLBoolean) + .build()); + } + return queryTypeBuilder.build(); + } + + private Set buildTypes() { + return typesProviders.stream() + .map(GraphQLTypesProvider::getTypes) + .flatMap(Collection::stream) + .collect(toSet()); + } + + private GraphQLObjectType buildMutationType() { + return buildObjectType("Mutation", new ArrayList<>(mutationProviders)); + } + + private GraphQLObjectType buildSubscriptionType() { + return buildObjectType("Subscription", new ArrayList<>(subscriptionProviders)); + } + + private GraphQLObjectType buildObjectType(String name, List providers) { + if (!providers.isEmpty()) { + final GraphQLObjectType.Builder typeBuilder = newObject().name(name) + .description("Root " + name.toLowerCase() + " type"); + + for (GraphQLFieldProvider provider : providers) { + provider.getFields().forEach(typeBuilder::field); + } + + if (!typeBuilder.build().getFieldDefinitions().isEmpty()) { + return typeBuilder.build(); + } + } + return null; + } + + void add(GraphQLQueryProvider provider) { + queryProviders.add(provider); + } + + void add(GraphQLMutationProvider provider) { + mutationProviders.add(provider); + } + + void add(GraphQLSubscriptionProvider provider) { + subscriptionProviders.add(provider); + } + + void add(GraphQLTypesProvider provider) { + typesProviders.add(provider); + } + + void remove(GraphQLQueryProvider provider) { + queryProviders.remove(provider); + } + + void remove(GraphQLMutationProvider provider) { + mutationProviders.remove(provider); + } + + void remove(GraphQLSubscriptionProvider provider) { + subscriptionProviders.remove(provider); + } + + void remove(GraphQLTypesProvider provider) { + typesProviders.remove(provider); + } + + GraphQLSchemaServletProvider getSchemaProvider() { + return schemaProvider; + } + + GraphQLConfiguration buildConfiguration() { + return GraphQLConfiguration + .with(buildInvocationInputFactory()) + .with(buildQueryInvoker()) + .with(buildObjectMapper()) + .with(listeners) + .build(); + } + + private GraphQLInvocationInputFactory buildInvocationInputFactory() { + return GraphQLInvocationInputFactory.newBuilder(this::getSchemaProvider) + .withGraphQLContextBuilder(contextBuilder) + .withGraphQLRootObjectBuilder(rootObjectBuilder) + .build(); + } + + private GraphQLQueryInvoker buildQueryInvoker() { + return GraphQLQueryInvoker.newBuilder() + .withPreparsedDocumentProvider(preparsedDocumentProvider) + .withInstrumentation(() -> instrumentationProvider.getInstrumentation()) + .withExecutionStrategyProvider(executionStrategyProvider).build(); + } + + private GraphQLObjectMapper buildObjectMapper() { + return GraphQLObjectMapper.newBuilder() + .withGraphQLErrorHandler(errorHandler) + .build(); + } + + void add(GraphQLServletListener listener) { + listeners.add(listener); + } + + void remove(GraphQLServletListener listener) { + listeners.remove(listener); + } +} diff --git a/graphql-java-servlet/src/main/java/graphql/kickstart/servlet/osgi/GraphQLFieldProvider.java b/graphql-java-servlet/src/main/java/graphql/kickstart/servlet/osgi/GraphQLFieldProvider.java new file mode 100644 index 00000000..1f625342 --- /dev/null +++ b/graphql-java-servlet/src/main/java/graphql/kickstart/servlet/osgi/GraphQLFieldProvider.java @@ -0,0 +1,10 @@ +package graphql.kickstart.servlet.osgi; + +import graphql.schema.GraphQLFieldDefinition; +import java.util.Collection; + +public interface GraphQLFieldProvider extends GraphQLProvider { + + Collection getFields(); + +} diff --git a/graphql-java-servlet/src/main/java/graphql/kickstart/servlet/osgi/GraphQLMutationProvider.java b/graphql-java-servlet/src/main/java/graphql/kickstart/servlet/osgi/GraphQLMutationProvider.java index b0b1b73c..1f245a7d 100644 --- a/graphql-java-servlet/src/main/java/graphql/kickstart/servlet/osgi/GraphQLMutationProvider.java +++ b/graphql-java-servlet/src/main/java/graphql/kickstart/servlet/osgi/GraphQLMutationProvider.java @@ -3,8 +3,12 @@ import graphql.schema.GraphQLFieldDefinition; import java.util.Collection; -public interface GraphQLMutationProvider extends GraphQLProvider { +public interface GraphQLMutationProvider extends GraphQLFieldProvider { Collection getMutations(); + default Collection getFields() { + return getMutations(); + } + } diff --git a/graphql-java-servlet/src/main/java/graphql/kickstart/servlet/osgi/GraphQLSubscriptionProvider.java b/graphql-java-servlet/src/main/java/graphql/kickstart/servlet/osgi/GraphQLSubscriptionProvider.java index b89b042f..a1cbbc50 100644 --- a/graphql-java-servlet/src/main/java/graphql/kickstart/servlet/osgi/GraphQLSubscriptionProvider.java +++ b/graphql-java-servlet/src/main/java/graphql/kickstart/servlet/osgi/GraphQLSubscriptionProvider.java @@ -3,7 +3,12 @@ import graphql.schema.GraphQLFieldDefinition; import java.util.Collection; -public interface GraphQLSubscriptionProvider extends GraphQLProvider { +public interface GraphQLSubscriptionProvider extends GraphQLFieldProvider { Collection getSubscriptions(); + + default Collection getFields() { + return getSubscriptions(); + } + } diff --git a/graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/OsgiGraphQLHttpServletSpec.groovy b/graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/OsgiGraphQLHttpServletSpec.groovy index 223c6a84..274762c7 100644 --- a/graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/OsgiGraphQLHttpServletSpec.groovy +++ b/graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/OsgiGraphQLHttpServletSpec.groovy @@ -4,14 +4,22 @@ import graphql.AssertException import graphql.annotations.annotationTypes.GraphQLField import graphql.annotations.annotationTypes.GraphQLName import graphql.annotations.processor.GraphQLAnnotations -import graphql.kickstart.servlet.osgi.GraphQLCodeRegistryProvider -import graphql.kickstart.servlet.osgi.GraphQLMutationProvider -import graphql.kickstart.servlet.osgi.GraphQLQueryProvider -import graphql.kickstart.servlet.osgi.GraphQLSubscriptionProvider -import graphql.schema.GraphQLCodeRegistry -import graphql.schema.GraphQLFieldDefinition -import graphql.schema.GraphQLInterfaceType -import spock.lang.Ignore +import graphql.execution.instrumentation.Instrumentation +import graphql.execution.instrumentation.InstrumentationState +import graphql.execution.instrumentation.SimpleInstrumentation +import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters +import graphql.kickstart.execution.GraphQLRequest +import graphql.kickstart.execution.config.ExecutionStrategyProvider +import graphql.kickstart.execution.config.InstrumentationProvider +import graphql.kickstart.execution.context.DefaultGraphQLContext +import graphql.kickstart.execution.context.GraphQLContext +import graphql.kickstart.servlet.context.GraphQLServletContextBuilder +import graphql.kickstart.servlet.core.GraphQLServletListener +import graphql.kickstart.servlet.core.GraphQLServletRootObjectBuilder +import graphql.kickstart.servlet.input.NoOpBatchInputPreProcessor +import graphql.kickstart.servlet.osgi.* +import graphql.schema.* +import org.dataloader.DataLoaderRegistry import spock.lang.Specification import static graphql.Scalars.GraphQLInt @@ -23,24 +31,23 @@ class OsgiGraphQLHttpServletSpec extends Specification { @Override Collection getQueries() { - List fieldDefinitions = new ArrayList<>(); + List fieldDefinitions = new ArrayList<>() fieldDefinitions.add(newFieldDefinition() .name("query") - .type(GraphQLAnnotations.object(Query.class)) + .type(new GraphQLAnnotations().object(Query.class)) .staticValue(new Query()) - .build()); - return fieldDefinitions; + .build()) + return fieldDefinitions } @GraphQLName("query") static class Query { @GraphQLField - public String field; + public String field } } - @Ignore def "query provider adds query objects"() { setup: OsgiGraphQLHttpServlet servlet = new OsgiGraphQLHttpServlet() @@ -49,20 +56,20 @@ class OsgiGraphQLHttpServletSpec extends Specification { GraphQLFieldDefinition query when: - query = servlet.getSchemaProvider().getSchema().getQueryType().getFieldDefinition("query") + query = servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getSchema().getQueryType().getFieldDefinition("query") then: - query.getType().getName() == "query" + query.getType().name == "query" when: - query = servlet.getSchemaProvider().getReadOnlySchema(null).getQueryType().getFieldDefinition("query") + query = servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getReadOnlySchema().getQueryType().getFieldDefinition("query") then: - query.getType().getName() == "query" + query.getType().name == "query" when: servlet.unbindQueryProvider(queryProvider) then: - servlet.getSchemaProvider().getSchema().getQueryType().getFieldDefinitions().isEmpty() - servlet.getSchemaProvider().getReadOnlySchema(null).getQueryType().getFieldDefinitions().isEmpty() + servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getSchema().getQueryType().getFieldDefinitions().get(0).name == "_empty" + servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getReadOnlySchema().getQueryType().getFieldDefinitions().get(0).name == "_empty" } static class TestMutationProvider implements GraphQLMutationProvider { @@ -80,30 +87,39 @@ class OsgiGraphQLHttpServletSpec extends Specification { when: servlet.bindMutationProvider(mutationProvider) then: - servlet.getSchemaProvider().getSchema().getMutationType().getFieldDefinition("int").getType() == GraphQLInt - servlet.getSchemaProvider().getReadOnlySchema(null).getMutationType() == null + servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getSchema().getMutationType().getFieldDefinition("int").getType() == GraphQLInt + servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getReadOnlySchema().getMutationType() == null when: servlet.unbindMutationProvider(mutationProvider) then: - servlet.getSchemaProvider().getSchema().getMutationType() == null + servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getSchema().getMutationType() == null + + when: + servlet.bindProvider(mutationProvider) + then: + servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getSchema().getMutationType().getFieldDefinition("int").getType() == GraphQLInt + servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getReadOnlySchema().getMutationType() == null + + when: + servlet.unbindProvider(mutationProvider) + then: + servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getSchema().getMutationType() == null } static class TestSubscriptionProvider implements GraphQLSubscriptionProvider { @Override Collection getSubscriptions() { - return Collections.singletonList(newFieldDefinition().name("subscription").type(GraphQLAnnotations.object(Subscription.class)).build()) + return Collections.singletonList(newFieldDefinition().name("subscription").type(new GraphQLAnnotations().object(Subscription.class)).build()) } - @GraphQLName("subscription") static class Subscription { @GraphQLField - public String field; + public String field } } - @Ignore def "subscription provider adds subscription objects"() { setup: OsgiGraphQLHttpServlet servlet = new OsgiGraphQLHttpServlet() @@ -112,25 +128,36 @@ class OsgiGraphQLHttpServletSpec extends Specification { GraphQLFieldDefinition subscription when: - subscription = servlet.getSchemaProvider().getSchema().getSubscriptionType().getFieldDefinition("subscription") + subscription = servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getSchema().getSubscriptionType().getFieldDefinition("subscription") then: subscription.getType().getName() == "subscription" when: - subscription = servlet.getSchemaProvider().getReadOnlySchema(null).getSubscriptionType().getFieldDefinition("subscription") + subscription = servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getReadOnlySchema().getSubscriptionType().getFieldDefinition("subscription") then: subscription.getType().getName() == "subscription" when: servlet.unbindSubscriptionProvider(subscriptionProvider) then: - servlet.getSchemaProvider().getSchema().getSubscriptionType() == null + servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getSchema().getSubscriptionType() == null + + when: + servlet.bindProvider(subscriptionProvider) + then: + def subscription2 = servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getReadOnlySchema().getSubscriptionType().getFieldDefinition("subscription") + subscription2.getType().getName() == "subscription" + + when: + servlet.unbindProvider(subscriptionProvider) + then: + servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getSchema().getSubscriptionType() == null } static class TestCodeRegistryProvider implements GraphQLCodeRegistryProvider { @Override GraphQLCodeRegistry getCodeRegistry() { - return GraphQLCodeRegistry.newCodeRegistry().typeResolver("Type", { env -> null }).build(); + return GraphQLCodeRegistry.newCodeRegistry().typeResolver("Type", { env -> null }).build() } } @@ -141,15 +168,206 @@ class OsgiGraphQLHttpServletSpec extends Specification { when: servlet.bindCodeRegistryProvider(codeRegistryProvider) - servlet.getSchemaProvider().getSchema().getCodeRegistry().getTypeResolver(GraphQLInterfaceType.newInterface().name("Type").build()) + servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getSchema().getCodeRegistry().getTypeResolver(GraphQLInterfaceType.newInterface().name("Type").build()) then: notThrown AssertException when: servlet.unbindCodeRegistryProvider(codeRegistryProvider) - servlet.getSchemaProvider().getSchema().getCodeRegistry().getTypeResolver(GraphQLInterfaceType.newInterface().name("Type").build()) + servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getSchema().getCodeRegistry().getTypeResolver(GraphQLInterfaceType.newInterface().name("Type").build()) then: thrown AssertException + when: + servlet.bindProvider(codeRegistryProvider) + servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getSchema().getCodeRegistry().getTypeResolver(GraphQLInterfaceType.newInterface().name("Type").build()) + then: + notThrown AssertException + + when: + servlet.unbindProvider(codeRegistryProvider) + servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getSchema().getCodeRegistry().getTypeResolver(GraphQLInterfaceType.newInterface().name("Type").build()) + then: + thrown AssertException + } + + def "schema update delay throws no exception"() { + setup: + OsgiGraphQLHttpServlet servlet = new OsgiGraphQLHttpServlet() + def config = Mock(OsgiGraphQLHttpServlet.Config) + + when: + config.schema_update_delay() >> 1 + servlet.activate(config) + servlet.updateSchema() + servlet.updateSchema() + servlet.deactivate() + + then: + noExceptionThrown() + } + + def "bind query provider adds query objects"() { + setup: + def servlet = new OsgiGraphQLHttpServlet() + def queryProvider = new TestQueryProvider() + def query + + when: + servlet.bindProvider(queryProvider) + query = servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getSchema().getQueryType().getFieldDefinition("query") + + then: + query.getType().name == "query" + + when: + query = servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getReadOnlySchema().getQueryType().getFieldDefinition("query") + + then: + query.getType().name == "query" + + when: + servlet.unbindProvider(queryProvider) + then: + null != servlet.getConfiguration().getInvocationInputFactory().getSchemaProvider().getSchema().getQueryType().getFieldDefinition("_empty") + } + + def "type provider adds types"() { + setup: + def servlet = new OsgiGraphQLHttpServlet() + def typesProvider = Mock(GraphQLTypesProvider) + def coercing = Mock(Coercing) + typesProvider.types >> [GraphQLScalarType.newScalar().name("Upload").coercing(coercing).build()] + + when: + servlet.bindTypesProvider(typesProvider) + + then: + def type = servlet.configuration.invocationInputFactory.schemaProvider.schema.getType("Upload") + type != null + type.name == "Upload" + type instanceof GraphQLScalarType + def scalarType = (GraphQLScalarType) type + scalarType.coercing == coercing + + when: + servlet.unbindTypesProvider(typesProvider) + + then: + null == servlet.configuration.invocationInputFactory.schemaProvider.schema.getType("Upload") + + when: + servlet.bindProvider(typesProvider) + then: + servlet.configuration.invocationInputFactory.schemaProvider.schema.getType("Upload").name == "Upload" + + when: + servlet.unbindProvider(typesProvider) + then: + null == servlet.configuration.invocationInputFactory.schemaProvider.schema.getType("Upload") + } + + def "servlet listener is bound and unbound"() { + setup: + def servlet = new OsgiGraphQLHttpServlet() + def listener = Mock(GraphQLServletListener) + + when: + servlet.bindServletListener(listener) + then: + servlet.configuration.listeners.contains(listener) + + when: + servlet.unbindServletListener(listener) + then: + !servlet.configuration.listeners.contains(listener) + } + + def "context builder is bound and unbound"() { + setup: + def servlet = new OsgiGraphQLHttpServlet() + def context = Mock(GraphQLContext) + context.getDataLoaderRegistry() >> new DataLoaderRegistry() + context.getSubject() >> Optional.empty() + def contextBuilder = Mock(GraphQLServletContextBuilder) + contextBuilder.build() >> context + def request = GraphQLRequest.createIntrospectionRequest() + + when: + servlet.setContextBuilder(contextBuilder) + then: + def invocationInput = servlet.configuration.invocationInputFactory.create(request) + invocationInput.executionInput.context == context + + when: + servlet.unsetContextBuilder(contextBuilder) + then: + servlet.configuration.invocationInputFactory.create(request).executionInput.context instanceof DefaultGraphQLContext + } + + def "root object builder is bound and unbound"() { + setup: + def servlet = new OsgiGraphQLHttpServlet() + def rootObject = Mock(Object) + def rootObjectBuilder = Mock(GraphQLServletRootObjectBuilder) + rootObjectBuilder.build() >> rootObject + def request = GraphQLRequest.createIntrospectionRequest() + + when: + servlet.setRootObjectBuilder(rootObjectBuilder) + then: + def invocationInput = servlet.configuration.invocationInputFactory.create(request) + invocationInput.executionInput.root == rootObject + + when: + servlet.unsetRootObjectBuilder(rootObjectBuilder) + then: + servlet.configuration.invocationInputFactory.create(request).executionInput.root != rootObject + } + + def "execution strategy is bound and unbound"() { + setup: + def servlet = new OsgiGraphQLHttpServlet() + def executionStrategy = Mock(ExecutionStrategyProvider) + def request = GraphQLRequest.createIntrospectionRequest() + + when: + servlet.setExecutionStrategyProvider(executionStrategy) + def invocationInput = servlet.configuration.invocationInputFactory.create(request) + servlet.configuration.graphQLInvoker.query(invocationInput) + + then: + 1 * executionStrategy.getQueryExecutionStrategy() + + when: + servlet.unsetExecutionStrategyProvider(executionStrategy) + def invocationInput2 = servlet.configuration.invocationInputFactory.create(request) + servlet.configuration.graphQLInvoker.query(invocationInput2) + + then: + 0 * executionStrategy.getQueryExecutionStrategy() + } + + def "instrumentation provider is bound and unbound"() { + setup: + def servlet = new OsgiGraphQLHttpServlet() + def instrumentation = new SimpleInstrumentation() + def instrumentationProvider = Mock(InstrumentationProvider) + instrumentationProvider.getInstrumentation() >> instrumentation + def request = GraphQLRequest.createIntrospectionRequest() + instrumentation.createState(_ as InstrumentationCreateStateParameters) >> Mock(InstrumentationState) + + when: + servlet.setInstrumentationProvider(instrumentationProvider) + def invocationInput = servlet.configuration.invocationInputFactory.create(request) + servlet.configuration.graphQLInvoker.query(invocationInput) + + then: + noExceptionThrown() + + when: + servlet.unsetInstrumentationProvider(instrumentationProvider) + then: + noExceptionThrown() } } diff --git a/graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/PartIOExceptionTest.groovy b/graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/PartIOExceptionTest.groovy new file mode 100644 index 00000000..4de3ede9 --- /dev/null +++ b/graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/PartIOExceptionTest.groovy @@ -0,0 +1,13 @@ +package graphql.kickstart.servlet + +import spock.lang.Specification + +class PartIOExceptionTest extends Specification { + + def "constructs"() { + when: + def e = new PartIOException("some message", new IOException()) + then: + e instanceof RuntimeException + } +} diff --git a/graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/SingleAsynchronousQueryResponseWriterTest.groovy b/graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/SingleAsynchronousQueryResponseWriterTest.groovy new file mode 100644 index 00000000..5a02ecf4 --- /dev/null +++ b/graphql-java-servlet/src/test/groovy/graphql/kickstart/servlet/SingleAsynchronousQueryResponseWriterTest.groovy @@ -0,0 +1,34 @@ +package graphql.kickstart.servlet + +import graphql.ExecutionResult +import graphql.kickstart.execution.GraphQLObjectMapper +import org.springframework.mock.web.MockAsyncContext +import spock.lang.Specification + +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +class SingleAsynchronousQueryResponseWriterTest extends Specification { + + def "result data is no publisher should"() { + given: + def result = Mock(ExecutionResult) + def objectMapper = Mock(GraphQLObjectMapper) + def writer = new SingleAsynchronousQueryResponseWriter(result, objectMapper, 100) + def request = Mock(HttpServletRequest) + def responseWriter = new PrintWriter(new StringWriter()) + def response = Mock(HttpServletResponse) + response.getWriter() >> responseWriter + def asyncContext = new MockAsyncContext(request, response) + request.getAsyncContext() >> asyncContext + request.isAsyncStarted() >> true + objectMapper.serializeResultAsJson(result) >> "{ }" + + when: + writer.write(request, response) + + then: + noExceptionThrown() + } + +} diff --git a/lombok.config b/lombok.config new file mode 100644 index 00000000..7a21e880 --- /dev/null +++ b/lombok.config @@ -0,0 +1 @@ +lombok.addLombokGeneratedAnnotation = true