diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultGraphQlClient.java b/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultGraphQlClient.java index ff74f2dc..c04303e5 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultGraphQlClient.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultGraphQlClient.java @@ -29,6 +29,7 @@ import reactor.core.publisher.Mono; import org.springframework.core.ParameterizedTypeReference; import org.springframework.graphql.GraphQlRequest; +import org.springframework.graphql.support.DocumentSource; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -69,20 +70,19 @@ final class DefaultGraphQlClient implements GraphQlClient { @Override public RequestSpec document(String document) { - return new DefaultRequestSpec(document, this.transport, this.jsonPathConfig); + return new DefaultRequestSpec(Mono.just(document), this.transport, this.jsonPathConfig); } @Override public RequestSpec documentName(String name) { - String document = this.documentSource.getDocument(name); - Assert.notNull(document, "Failed to find document for name: '" + name + "'"); + Mono document = this.documentSource.getDocument(name); return new DefaultRequestSpec(document, this.transport, this.jsonPathConfig); } private static final class DefaultRequestSpec implements RequestSpec { - private final String document; + private final Mono documentMono; @Nullable private String operationName; @@ -93,9 +93,9 @@ final class DefaultGraphQlClient implements GraphQlClient { private final Configuration jsonPathConfig; - DefaultRequestSpec(String document, GraphQlTransport transport, Configuration jsonPathConfig) { - Assert.hasText(document, "'document' is required"); - this.document = document; + DefaultRequestSpec(Mono documentMono, GraphQlTransport transport, Configuration jsonPathConfig) { + Assert.notNull(documentMono, "'documentMono' is required"); + this.documentMono = documentMono; this.transport = transport; this.jsonPathConfig = jsonPathConfig; } @@ -114,18 +114,21 @@ final class DefaultGraphQlClient implements GraphQlClient { @Override public Mono execute() { - return this.transport.execute(createRequest()) + return getRequestMono() + .flatMap(this.transport::execute) .map(payload -> new DefaultResponseSpec(payload, this.jsonPathConfig)); } @Override public Flux executeSubscription() { - return this.transport.executeSubscription(createRequest()) + return getRequestMono() + .flatMapMany(this.transport::executeSubscription) .map(payload -> new DefaultResponseSpec(payload, this.jsonPathConfig)); } - private GraphQlRequest createRequest() { - return new GraphQlRequest(this.document, this.operationName, this.variables); + private Mono getRequestMono() { + return this.documentMono.map(document -> + new GraphQlRequest(document, this.operationName, this.variables)); } } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultGraphQlClientBuilder.java b/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultGraphQlClientBuilder.java index c413d563..67ac8e61 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultGraphQlClientBuilder.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/DefaultGraphQlClientBuilder.java @@ -19,6 +19,8 @@ import com.jayway.jsonpath.Configuration; import com.jayway.jsonpath.spi.json.JacksonJsonProvider; import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider; +import org.springframework.graphql.support.DocumentSource; +import org.springframework.graphql.support.ResourceDocumentSource; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/GraphQlClient.java b/spring-graphql/src/main/java/org/springframework/graphql/client/GraphQlClient.java index 1b4c5564..8935ab0a 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/GraphQlClient.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/GraphQlClient.java @@ -23,6 +23,8 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.graphql.support.DocumentSource; +import org.springframework.graphql.support.ResourceDocumentSource; import org.springframework.lang.Nullable; /** diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/HttpGraphQlTransport.java b/spring-graphql/src/main/java/org/springframework/graphql/client/HttpGraphQlTransport.java index ee5c5408..6ffd963e 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/HttpGraphQlTransport.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/HttpGraphQlTransport.java @@ -23,6 +23,7 @@ import reactor.core.publisher.Mono; import org.springframework.core.ParameterizedTypeReference; import org.springframework.graphql.GraphQlRequest; +import org.springframework.graphql.support.MapExecutionResult; import org.springframework.http.MediaType; import org.springframework.util.Assert; import org.springframework.web.reactive.function.client.WebClient; @@ -80,7 +81,7 @@ public class HttpGraphQlTransport implements GraphQlTransport { .bodyValue(request.toMap()) .retrieve() .bodyToMono(MAP_TYPE) - .map(MapExecutionResult::new); + .map(MapExecutionResult::from); } @Override diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/ResourceDocumentSource.java b/spring-graphql/src/main/java/org/springframework/graphql/client/ResourceDocumentSource.java deleted file mode 100644 index b331f370..00000000 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/ResourceDocumentSource.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2002-2022 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.client; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.Resource; -import org.springframework.util.FileCopyUtils; - -/** - * {@link DocumentSource} that looks under a set of locations for a - * {@link Resource} with the document name and a list of configured extensions. - * - * @author Rossen Stoyanchev - * @since 1.0.0 - */ -public class ResourceDocumentSource implements DocumentSource { - - private static final List FILE_EXTENSIONS = Arrays.asList(".graphql", ".gql"); - - - private final List locations; - - private final List extensions; - - - /** - * Default constructor to look under {@code graphql/} on the classpath for - * resources with extensions ".graphql" and ".gql". - */ - public ResourceDocumentSource() { - this(Collections.singletonList(new ClassPathResource("graphql/"))); - } - - /** - * Constructor with custom locations with extensions ".graphql" and ".gql". - */ - public ResourceDocumentSource(List locations) { - this(locations, FILE_EXTENSIONS); - } - - /** - * Constructor with given locations and extensions. - */ - public ResourceDocumentSource(List locations, List extensions) { - this.locations = new ArrayList<>(locations); - this.extensions = new ArrayList<>(extensions); - } - - - /** - * Return the configured locations. - */ - public List getLocations() { - return this.locations; - } - - /** - * Return the configured extensions. - */ - public List getExtensions() { - return this.extensions; - } - - - @Override - public String getDocument(String name) { - return this.locations.stream() - .flatMap(location -> this.extensions.stream().map(ext -> getRelativeResource(location, name, ext))) - .filter(Resource::exists) - .findFirst() - .map(resource -> { - try { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - FileCopyUtils.copy(resource.getInputStream(), out); - return new String(out.toByteArray(), StandardCharsets.UTF_8); - } - catch (IOException ex) { - throw new IllegalArgumentException( - "Found resource: " + resource.getDescription() + " but failed to read it", ex); - } - }) - .orElse(null); - } - - private Resource getRelativeResource(Resource location, String name, String ext) { - try { - return location.createRelative(name + ext); - } - catch (IOException ex) { - throw new IllegalStateException(ex); - } - } - -} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/WebSocketGraphQlTransport.java b/spring-graphql/src/main/java/org/springframework/graphql/client/WebSocketGraphQlTransport.java index 8b13d103..08e4b690 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/WebSocketGraphQlTransport.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/WebSocketGraphQlTransport.java @@ -36,6 +36,8 @@ import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; import org.springframework.graphql.GraphQlRequest; +import org.springframework.graphql.support.MapExecutionResult; +import org.springframework.graphql.support.MapGraphQlError; import org.springframework.graphql.web.webflux.GraphQlWebSocketMessage; import org.springframework.http.HttpHeaders; import org.springframework.http.codec.ClientCodecConfigurer; @@ -614,7 +616,9 @@ public final class WebSocketGraphQlTransport implements GraphQlTransport { return; } - ExecutionResult result = new MapExecutionResult(message.getPayload()); + Map resultMap = message.getPayloadOrDefault(Collections.emptyMap()); + ExecutionResult result = MapExecutionResult.from(resultMap); + Sinks.EmitResult emitResult = (sink != null ? sink.tryEmitValue(result) : streamingSink.tryEmitNext(result)); if (emitResult.isFailure()) { // Just log: cannot overflow, is serialized, and cancel is handled in doOnCancel @@ -640,15 +644,15 @@ public final class WebSocketGraphQlTransport implements GraphQlTransport { return; } - List> payload = message.getPayload(); + List> payload = message.getPayloadOrDefault(Collections.emptyList()); Sinks.EmitResult emitResult; if (sink != null) { - ExecutionResult result = new MapExecutionResult(Collections.singletonMap("errors", payload)); + ExecutionResult result = MapExecutionResult.forErrorsOnly(payload); emitResult = sink.tryEmitValue(result); } else { - List graphQLErrors = MapGraphQlError.fromMapList(payload); + List graphQLErrors = MapGraphQlError.from(payload); Exception ex = new SubscriptionErrorException(graphQLErrors); emitResult = streamingSink.tryEmitError(ex); } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/support/CachingDocumentSource.java b/spring-graphql/src/main/java/org/springframework/graphql/support/CachingDocumentSource.java new file mode 100644 index 00000000..73cf5260 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/support/CachingDocumentSource.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2022 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.support; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import reactor.core.publisher.Mono; + +/** + * Base class for {@link DocumentSource} implementations providing support for + * caching loaded documents. + * + * @author Rossen Stoyanchev + * @since 1.0.0 + */ +public class CachingDocumentSource implements DocumentSource { + + private final DocumentSource delegate; + + private boolean cacheEnabled = true; + + private final Map> documentCache = new ConcurrentHashMap<>(); + + + /** + * Constructor with the {@code DocumentSource} to actually load documents. + */ + public CachingDocumentSource(DocumentSource delegate) { + this.delegate = delegate; + } + + + /** + * Enable or disable caching of resolved documents. + *

By default, set to {@code true}. + * @param cacheEnabled enable if {@code true} and disable if {@code false} + */ + public void setCacheEnabled(boolean cacheEnabled) { + this.cacheEnabled = cacheEnabled; + if (!cacheEnabled) { + this.documentCache.clear(); + } + } + + /** + * Whether {@link #setCacheEnabled(boolean) caching} is enabled. + */ + public boolean isCacheEnabled() { + return cacheEnabled; + } + + + @Override + public Mono getDocument(String name) { + return (isCacheEnabled() ? + this.documentCache.computeIfAbsent(name, k -> this.delegate.getDocument(name).cache()) : + this.delegate.getDocument(name)); + } + + /** + * Remove all entries from the document cache. + */ + public void clearCache() { + this.documentCache.clear(); + } + +} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/DocumentSource.java b/spring-graphql/src/main/java/org/springframework/graphql/support/DocumentSource.java similarity index 77% rename from spring-graphql/src/main/java/org/springframework/graphql/client/DocumentSource.java rename to spring-graphql/src/main/java/org/springframework/graphql/support/DocumentSource.java index 1446c4ee..26028d64 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/DocumentSource.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/support/DocumentSource.java @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.graphql.client; +package org.springframework.graphql.support; -import org.springframework.lang.Nullable; +import reactor.core.publisher.Mono; /** - * Strategy to locate a GraphQL document identified by name. + * Strategy to locate a GraphQL document by a name. * * @author Rossen Stoyanchev * @since 1.0.0 @@ -28,9 +28,8 @@ public interface DocumentSource { /** * Return the document that matches the given name. * @param name the name to use for the lookup - * @return the document, or {@code null} + * @return {@code Mono} that provides the document or returns an error */ - @Nullable - String getDocument(String name); + Mono getDocument(String name); } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/MapExecutionResult.java b/spring-graphql/src/main/java/org/springframework/graphql/support/MapExecutionResult.java similarity index 52% rename from spring-graphql/src/main/java/org/springframework/graphql/client/MapExecutionResult.java rename to spring-graphql/src/main/java/org/springframework/graphql/support/MapExecutionResult.java index 6d67c86b..fdf7b5d6 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/MapExecutionResult.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/support/MapExecutionResult.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.graphql.client; +package org.springframework.graphql.support; import java.util.Collections; import java.util.List; @@ -24,7 +24,7 @@ import graphql.ExecutionResult; import graphql.ExecutionResultImpl; import graphql.GraphQLError; -import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * Implementation of {@link ExecutionResult} backed by a {@link Map}. @@ -32,16 +32,18 @@ import org.springframework.lang.Nullable; * @author Rossen Stoyanchev * @since 1.0.0 */ -final class MapExecutionResult implements ExecutionResult { +public final class MapExecutionResult implements ExecutionResult { - private final Map map; + private final Map resultMap; private final List errors; - MapExecutionResult(@Nullable Map map) { - this.map = (map != null ? map : Collections.emptyMap()); - this.errors = MapGraphQlError.fromResultMap(map); + @SuppressWarnings("unchecked") + private MapExecutionResult(Map resultMap) { + Assert.notNull(resultMap, "'resultMap' is required"); + this.resultMap = resultMap; + this.errors = MapGraphQlError.from((List>) resultMap.get("errors")); } @@ -53,18 +55,18 @@ final class MapExecutionResult implements ExecutionResult { @SuppressWarnings("unchecked") @Override public T getData() { - return (T) this.map.get("data"); + return (T) this.resultMap.get("data"); } @Override public boolean isDataPresent() { - return (this.map.get("data") != null); + return (this.resultMap.get("data") != null); } @SuppressWarnings("unchecked") @Override public Map getExtensions() { - return (Map) this.map.get("extensions"); + return (Map) this.resultMap.get("extensions"); } @Override @@ -74,25 +76,43 @@ final class MapExecutionResult implements ExecutionResult { @Override public boolean equals(Object other) { - return (other instanceof MapExecutionResult && this.map.equals(((MapExecutionResult) other).map)); + return (other instanceof MapExecutionResult && + this.resultMap.equals(((MapExecutionResult) other).resultMap)); } @Override public int hashCode() { - return this.map.hashCode(); + return this.resultMap.hashCode(); } @Override public String toString() { - return this.map.toString(); + return this.resultMap.toString(); } /** - * Static factory method to create an instance of this class. + * Create an instance from an {@code ExecutionResult} serialized to map via + * {@link ExecutionResult#toSpecification()}. */ - public static ExecutionResult forData(@Nullable Map map) { + public static ExecutionResult from(Map map) { + return new MapExecutionResult(map); + } + + /** + * Create an {@code ExecutionResult} with a "data" key that returns the + * given map. + */ + public static ExecutionResult forDataOnly(Map map) { return new MapExecutionResult(Collections.singletonMap("data", map)); } + /** + * Create an {@code ExecutionResult} with an "errors" key that returns the + * given serialized errors. + */ + public static ExecutionResult forErrorsOnly(List> errors) { + return new MapExecutionResult(Collections.singletonMap("errors", errors)); + } + } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/MapGraphQlError.java b/spring-graphql/src/main/java/org/springframework/graphql/support/MapGraphQlError.java similarity index 80% rename from spring-graphql/src/main/java/org/springframework/graphql/client/MapGraphQlError.java rename to spring-graphql/src/main/java/org/springframework/graphql/support/MapGraphQlError.java index 079932c9..dc65de09 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/MapGraphQlError.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/support/MapGraphQlError.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.graphql.client; +package org.springframework.graphql.support; import java.util.Collections; import java.util.List; @@ -28,6 +28,7 @@ import graphql.language.SourceLocation; import org.springframework.graphql.execution.ErrorType; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; /** @@ -37,7 +38,7 @@ import org.springframework.util.CollectionUtils; * @since 1.0.0 */ @SuppressWarnings("serial") -final class MapGraphQlError implements GraphQLError { +public final class MapGraphQlError implements GraphQLError { private final Map errorMap; @@ -45,6 +46,7 @@ final class MapGraphQlError implements GraphQLError { private MapGraphQlError(Map errorMap) { + Assert.notNull(errorMap, "'errorMap' is required"); this.errorMap = errorMap; this.locations = initLocations(errorMap); } @@ -64,8 +66,6 @@ final class MapGraphQlError implements GraphQLError { } - - @Override @Nullable public String getMessage() { @@ -119,6 +119,7 @@ final class MapGraphQlError implements GraphQLError { return GraphqlErrorHelper.toSpecification(this); } + @SuppressWarnings("EqualsWhichDoesntCheckParameterClass") @Override public boolean equals(Object other) { return GraphqlErrorHelper.equals(this, other); @@ -136,26 +137,12 @@ final class MapGraphQlError implements GraphQLError { /** - * Static factory method to create an instance from a list of maps, each - * containing an error. + * Create a list of {@code GraphQlError} instances from the given + * deserialized content. */ - public static List fromMapList(@Nullable List> errorMaps) { - if (CollectionUtils.isEmpty(errorMaps)) { - return Collections.emptyList(); - } - return errorMaps.stream().map(MapGraphQlError::new).collect(Collectors.toList()); - } - - /** - * Static factory method to create an instance from an - * {@link graphql.ExecutionResult} map. - */ - @SuppressWarnings("unchecked") - public static List fromResultMap(@Nullable Map map) { - if (map == null) { - return Collections.emptyList(); - } - return MapGraphQlError.fromMapList((List>) map.get("errors")); + public static List from(@Nullable List> errors) { + errors = (errors != null ? errors : Collections.emptyList()); + return errors.stream().map(MapGraphQlError::new).collect(Collectors.toList()); } } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/support/ResourceDocumentSource.java b/spring-graphql/src/main/java/org/springframework/graphql/support/ResourceDocumentSource.java new file mode 100644 index 00000000..e76aea1e --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/support/ResourceDocumentSource.java @@ -0,0 +1,128 @@ +/* + * Copyright 2002-2022 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.support; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.util.FileCopyUtils; + +/** + * {@link DocumentSource} that looks for a document {@link Resource} under a set + * of locations and trying a number of different file extension. + * + * @author Rossen Stoyanchev + * @since 1.0.0 + */ +public class ResourceDocumentSource implements DocumentSource { + + /** + * The default file extensions, ".graphql" and ".gql". + */ + public static final List FILE_EXTENSIONS = Arrays.asList(".graphql", ".gql"); + + + private final List locations; + + private final List extensions; + + + /** + * Default constructor that sets the location to {@code "classpath:graphql/"} + * and the extensions to ".graphql" and ".gql". + */ + public ResourceDocumentSource() { + this(Collections.singletonList(new ClassPathResource("graphql/")), FILE_EXTENSIONS); + } + + /** + * Constructor with given locations and extensions. + */ + public ResourceDocumentSource(List locations, List extensions) { + this.locations = new ArrayList<>(locations); + this.extensions = new ArrayList<>(extensions); + } + + + /** + * Return the configured locations where to check for documents. + */ + public List getLocations() { + return this.locations; + } + + /** + * Return the file extensions to try when checking for documents by name. + */ + public List getExtensions() { + return this.extensions; + } + + + @Override + public Mono getDocument(String name) { + return Flux.fromIterable(this.locations) + .flatMapIterable(location -> getCandidateResources(name, location)) + .filter(Resource::exists) + .next() + .map(this::resourceToString) + .switchIfEmpty(Mono.fromRunnable(() -> { + throw new IllegalStateException( + "Failed to find document, name='" + name + "', under location(s)=" + + this.locations.stream().map(Resource::toString).collect(Collectors.toList())); + })) + .subscribeOn(Schedulers.boundedElastic()); + } + + private List getCandidateResources(String name, Resource location) { + return this.extensions.stream() + .map(ext -> { + try { + return location.createRelative(name + ext); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + }) + .collect(Collectors.toList()); + } + + private String resourceToString(Resource resource) { + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + FileCopyUtils.copy(resource.getInputStream(), out); + return new String(out.toByteArray(), StandardCharsets.UTF_8); + } + catch (IOException ex) { + throw new IllegalArgumentException( + "Found resource: " + resource.getDescription() + " but failed to read it", ex); + } + } + +} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/support/package-info.java b/spring-graphql/src/main/java/org/springframework/graphql/support/package-info.java new file mode 100644 index 00000000..02c31774 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/support/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright 2020-2022 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. + */ + +/** + * Support classes for Spring GraphQL. + */ +@NonNullApi +@NonNullFields +package org.springframework.graphql.support; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-graphql/src/test/java/org/springframework/graphql/client/MockWebSocketGraphQlTransportTests.java b/spring-graphql/src/test/java/org/springframework/graphql/client/MockWebSocketGraphQlTransportTests.java index 84d4fb7f..19943d29 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/client/MockWebSocketGraphQlTransportTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/client/MockWebSocketGraphQlTransportTests.java @@ -34,6 +34,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import org.springframework.graphql.GraphQlRequest; +import org.springframework.graphql.support.MapExecutionResult; import org.springframework.graphql.web.webflux.GraphQlWebSocketMessage; import org.springframework.http.HttpHeaders; import org.springframework.web.reactive.socket.CloseStatus; @@ -64,9 +65,9 @@ public class MockWebSocketGraphQlTransportTests { private final WebSocketGraphQlTransport transport = WebSocketGraphQlTransport.builder(URI.create("/"), this.testClient).build(); - private final ExecutionResult result1 = MapExecutionResult.forData(Collections.singletonMap("key1", "value1")); + private final ExecutionResult result1 = MapExecutionResult.forDataOnly(Collections.singletonMap("key1", "value1")); - private final ExecutionResult result2 = MapExecutionResult.forData(Collections.singletonMap("key2", "value2")); + private final ExecutionResult result2 = MapExecutionResult.forDataOnly(Collections.singletonMap("key2", "value2")); @Test diff --git a/spring-graphql/src/test/java/org/springframework/graphql/support/CachingDocumentSourceTests.java b/spring-graphql/src/test/java/org/springframework/graphql/support/CachingDocumentSourceTests.java new file mode 100644 index 00000000..10df4592 --- /dev/null +++ b/spring-graphql/src/test/java/org/springframework/graphql/support/CachingDocumentSourceTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2022 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.support; + +import java.util.Collections; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link ResourceDocumentSource}. + * @author Rossen Stoyanchev + */ +public class CachingDocumentSourceTests { + + private CachingDocumentSource source; + + + @BeforeEach + void setUp() { + DocumentSource resourceSource = new ResourceDocumentSource( + Collections.singletonList(new ClassPathResource("books/")), + ResourceDocumentSource.FILE_EXTENSIONS); + + this.source = new CachingDocumentSource(resourceSource); + } + + + @Test + void cachingDefault() { + assertThat(this.source.isCacheEnabled()).isTrue(); + } + + @Test + void cachingOn() { + this.source.setCacheEnabled(true); + + Mono documentMono1 = source.getDocument("book-document"); + Mono documentMono2 = source.getDocument("book-document"); + + assertThat(documentMono1).isSameAs(documentMono2); + + String document1 = documentMono1.block(); + String document2 = documentMono2.block(); + + assertThat(document1).isSameAs(document2); + } + + @Test + void cachingOff() { + this.source.setCacheEnabled(false); + + Mono documentMono1 = source.getDocument("book-document"); + Mono documentMono2 = source.getDocument("book-document"); + + assertThat(documentMono1).isNotSameAs(documentMono2); + + String document1 = documentMono1.block(); + String document2 = documentMono2.block(); + + assertThat(document1).isNotSameAs(document2); + } + +} diff --git a/spring-graphql/src/test/java/org/springframework/graphql/support/ResourceDocumentSourceTests.java b/spring-graphql/src/test/java/org/springframework/graphql/support/ResourceDocumentSourceTests.java new file mode 100644 index 00000000..19916e89 --- /dev/null +++ b/spring-graphql/src/test/java/org/springframework/graphql/support/ResourceDocumentSourceTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2022 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.support; + +import java.time.Duration; +import java.util.Collections; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import reactor.test.StepVerifier; + +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link ResourceDocumentSource}. + * @author Rossen Stoyanchev + */ +public class ResourceDocumentSourceTests { + + private ResourceDocumentSource source; + + + @BeforeEach + void setUp() { + this.source = new ResourceDocumentSource( + Collections.singletonList(new ClassPathResource("books/")), + ResourceDocumentSource.FILE_EXTENSIONS); + } + + + @Test + void getDocument() { + String content = this.source.getDocument("book-document").block(Duration.ofSeconds(5)); + assertThat(content).startsWith("bookById(id:\"1\""); + } + + @Test + void getDocumentNotFound() { + StepVerifier.create(this.source.getDocument("invalid")) + .expectErrorMessage( + "Failed to find document, name='invalid', " + + "under location(s)=[class path resource [books/]]") + .verify(Duration.ofSeconds(5)); + } + +} diff --git a/spring-graphql/src/test/resources/books/book-document.graphql b/spring-graphql/src/test/resources/books/book-document.graphql new file mode 100644 index 00000000..12e4347d --- /dev/null +++ b/spring-graphql/src/test/resources/books/book-document.graphql @@ -0,0 +1,4 @@ +bookById(id:"1") { + id + name +} \ No newline at end of file