From 625f0b685bfb9836a236d171c938e3874ba37aec Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 27 Aug 2021 20:36:05 +0200 Subject: [PATCH] Add FailureAnalyzer for schema location issues Prior to this commit, the schema locations configured with `spring.graphql.schema.locations` could fail in several cases: * one of the provided locations cannot be resolved as a Resource * none of the provided locations holds an actual schema file The latter can happen if several classpath locations match a given location and the wrong one is considered first. This commit introduces a new `MissingSchemaException` that holds the locations information. We're adding the relevant FailureAnalyzer to help developers troubleshoot such configurations issues. Fixes gh-117 --- .../boot/GraphQlAutoConfiguration.java | 14 +++- .../graphql/boot/MissingSchemaException.java | 84 +++++++++++++++++++ ...MissingSchemaExceptionFailureAnalyzer.java | 39 +++++++++ .../main/resources/META-INF/spring.factories | 4 + .../boot/GraphQlAutoConfigurationTests.java | 2 +- 5 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/MissingSchemaException.java create mode 100644 graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/MissingSchemaExceptionFailureAnalyzer.java diff --git a/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/GraphQlAutoConfiguration.java b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/GraphQlAutoConfiguration.java index 628e90d3..fe84b7ec 100644 --- a/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/GraphQlAutoConfiguration.java +++ b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/GraphQlAutoConfiguration.java @@ -24,6 +24,8 @@ import java.util.stream.Collectors; import graphql.GraphQL; import graphql.execution.instrumentation.Instrumentation; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -51,6 +53,8 @@ import org.springframework.graphql.execution.RuntimeWiringConfigurer; @EnableConfigurationProperties(GraphQlProperties.class) public class GraphQlAutoConfiguration { + private static final Log logger = LogFactory.getLog(GraphQlAutoConfiguration.class); + private static final String[] SCHEMA_FILES_EXTENSIONS = new String[] {"*.graphqls", "*.graphql", "*.gql", "*.gqls"}; @Bean @@ -74,9 +78,17 @@ public class GraphQlAutoConfiguration { List schemaResources = new ArrayList<>(); for (String location : schemaLocations) { for (String extension : SCHEMA_FILES_EXTENSIONS) { - schemaResources.addAll(Arrays.asList(resolver.getResources(location + extension))); + try { + schemaResources.addAll(Arrays.asList(resolver.getResources(location + extension))); + } + catch (IOException ex) { + logger.debug("Could not resolve schema location: '" + location + extension + "'", ex); + } } } + if (schemaResources.isEmpty()) { + throw new MissingSchemaException(schemaLocations, resolver); + } return schemaResources; } diff --git a/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/MissingSchemaException.java b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/MissingSchemaException.java new file mode 100644 index 00000000..94099a47 --- /dev/null +++ b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/MissingSchemaException.java @@ -0,0 +1,84 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.graphql.boot; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.core.io.support.ResourcePatternResolver; + +/** + * {@link MissingSchemaException} thrown when no schema can be found in the provided locations. + * + * @author Brian Clozel + */ +public class MissingSchemaException extends RuntimeException { + + private final List schemaLocations; + + public MissingSchemaException(List locations, ResourcePatternResolver resolver) { + List providedLocations = new ArrayList<>(); + for (String location : locations) { + try { + String uri = resolver.getResource(location).getURI().toASCIIString(); + providedLocations.add(new SchemaLocation(location, uri)); + } + catch (IOException ex) { + providedLocations.add(new SchemaLocation(location, "")); + } + } + this.schemaLocations = Collections.unmodifiableList(providedLocations); + } + + /** + * Return the list of provided locations where to look for schemas. + */ + public List getSchemaLocations() { + return this.schemaLocations; + } + + /** + * The location where to look for schemas. + */ + public static class SchemaLocation { + + private final String location; + + private final String uri; + + SchemaLocation(String location, String uri) { + this.location = location; + this.uri = uri; + } + + /** + * Return the location String to be resolved by a {@link ResourcePatternResolver}. + */ + public String getLocation() { + return this.location; + } + + /** + * Return the resolved URI String for this location, an empty String if resolution failed. + */ + public String getUri() { + return this.uri; + } + } +} diff --git a/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/MissingSchemaExceptionFailureAnalyzer.java b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/MissingSchemaExceptionFailureAnalyzer.java new file mode 100644 index 00000000..9cfbf84d --- /dev/null +++ b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/MissingSchemaExceptionFailureAnalyzer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.graphql.boot; + +import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; +import org.springframework.boot.diagnostics.FailureAnalysis; + +/** + * An implementation of {@link AbstractFailureAnalyzer} to analyze failures caused by + * {@link MissingSchemaException}. + * + * @author Brian Clozel + */ +class MissingSchemaExceptionFailureAnalyzer extends AbstractFailureAnalyzer { + + @Override + protected FailureAnalysis analyze(Throwable rootFailure, MissingSchemaException cause) { + String message = "Could not find any GraphQL schema file under configured locations."; + StringBuilder action = new StringBuilder("Check that the following locations contain schema files: " + System.lineSeparator()); + for (MissingSchemaException.SchemaLocation schemaLocation : cause.getSchemaLocations()) { + action.append(String.format("- '%s' (%s)" + System.lineSeparator(), schemaLocation.getUri(), schemaLocation.getLocation())); + } + return new FailureAnalysis(message, action.toString(), cause); + } +} diff --git a/graphql-spring-boot-starter/src/main/resources/META-INF/spring.factories b/graphql-spring-boot-starter/src/main/resources/META-INF/spring.factories index 4ed3b560..937f2d96 100644 --- a/graphql-spring-boot-starter/src/main/resources/META-INF/spring.factories +++ b/graphql-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -9,6 +9,10 @@ org.springframework.graphql.boot.security.GraphQlWebMvcSecurityAutoConfiguration org.springframework.graphql.boot.data.GraphQlWebMvcQuerydslAutoConfiguration,\ org.springframework.graphql.boot.data.GraphQlWebFluxQuerydslAutoConfiguration +# Failure Analyzers +org.springframework.boot.diagnostics.FailureAnalyzer=\ +org.springframework.graphql.boot.MissingSchemaExceptionFailureAnalyzer + # Spring Test @AutoConfigureGraphQlTester org.springframework.graphql.boot.test.tester.AutoConfigureGraphQlTester=\ org.springframework.graphql.boot.test.tester.WebTestClientMockMvcAutoConfiguration,\ diff --git a/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/GraphQlAutoConfigurationTests.java b/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/GraphQlAutoConfigurationTests.java index 90ef180f..e5a7bf82 100644 --- a/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/GraphQlAutoConfigurationTests.java +++ b/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/GraphQlAutoConfigurationTests.java @@ -40,7 +40,7 @@ class GraphQlAutoConfigurationTests { void shouldFailWhenSchemaFileIsMissing() { this.contextRunner.run((context) -> { assertThat(context).hasFailed(); - assertThat(context).getFailure().getRootCause().hasMessage("'schemaResources' should not be empty"); + assertThat(context).getFailure().getRootCause().isInstanceOf(MissingSchemaException.class); }); }