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
This commit is contained in:
Brian Clozel
2021-08-27 20:36:05 +02:00
parent 73aec66200
commit 625f0b685b
5 changed files with 141 additions and 2 deletions

View File

@@ -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<Resource> 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;
}

View File

@@ -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<SchemaLocation> schemaLocations;
public MissingSchemaException(List<String> locations, ResourcePatternResolver resolver) {
List<SchemaLocation> 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<SchemaLocation> 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;
}
}
}

View File

@@ -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<MissingSchemaException> {
@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);
}
}

View File

@@ -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,\

View File

@@ -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);
});
}