diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultHttpGraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultHttpGraphQlTester.java index ece8dc28..ccb31e19 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultHttpGraphQlTester.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultHttpGraphQlTester.java @@ -20,7 +20,6 @@ package org.springframework.graphql.test.tester; import java.net.URI; import java.util.function.Consumer; -import org.springframework.graphql.client.CodecMappingProvider; import org.springframework.http.HttpHeaders; import org.springframework.http.codec.CodecConfigurer; import org.springframework.test.web.reactive.server.WebTestClient; @@ -119,7 +118,7 @@ final class DefaultHttpGraphQlTester extends AbstractDelegatingGraphQlTester imp private void registerJsonPathMappingProvider() { this.webTestClientBuilder.codecs(codecConfigurer -> configureJsonPathConfig(config -> { - CodecMappingProvider provider = new CodecMappingProvider(codecConfigurer); + EncoderDecoderMappingProvider provider = new EncoderDecoderMappingProvider(codecConfigurer); return config.mappingProvider(provider); })); } diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebGraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebGraphQlTester.java index f0e0bdd9..ac4caba0 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebGraphQlTester.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebGraphQlTester.java @@ -21,7 +21,6 @@ import java.net.URI; import java.util.Arrays; import java.util.function.Consumer; -import org.springframework.graphql.client.CodecMappingProvider; import org.springframework.graphql.web.WebGraphQlHandler; import org.springframework.http.HttpHeaders; import org.springframework.http.codec.ClientCodecConfigurer; @@ -134,7 +133,7 @@ final class DefaultWebGraphQlTester extends AbstractDelegatingGraphQlTester impl private void registerJsonPathMappingProvider() { configureJsonPathConfig(jsonPathConfig -> { - CodecMappingProvider provider = new CodecMappingProvider(this.codecConfigurer); + EncoderDecoderMappingProvider provider = new EncoderDecoderMappingProvider(this.codecConfigurer); return jsonPathConfig.mappingProvider(provider); }); } diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebSocketGraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebSocketGraphQlTester.java index 0161672d..cc70b823 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebSocketGraphQlTester.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebSocketGraphQlTester.java @@ -25,7 +25,6 @@ import reactor.core.publisher.Mono; import org.springframework.graphql.GraphQlRequest; import org.springframework.graphql.GraphQlResponse; -import org.springframework.graphql.client.CodecMappingProvider; import org.springframework.graphql.client.GraphQlClient; import org.springframework.graphql.client.GraphQlTransport; import org.springframework.graphql.client.WebSocketGraphQlClient; @@ -150,7 +149,7 @@ final class DefaultWebSocketGraphQlTester extends AbstractDelegatingGraphQlTeste private void registerJsonPathMappingProvider() { this.graphQlClientBuilder.codecConfigurer(codecConfigurer -> { configureJsonPathConfig(jsonPathConfig -> { - CodecMappingProvider provider = new CodecMappingProvider(codecConfigurer); + EncoderDecoderMappingProvider provider = new EncoderDecoderMappingProvider(codecConfigurer); return jsonPathConfig.mappingProvider(provider); }); }); diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/CodecMappingProvider.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/EncoderDecoderMappingProvider.java similarity index 67% rename from spring-graphql/src/main/java/org/springframework/graphql/client/CodecMappingProvider.java rename to spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/EncoderDecoderMappingProvider.java index 75b11111..280cd27a 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/CodecMappingProvider.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/EncoderDecoderMappingProvider.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.graphql.client; +package org.springframework.graphql.test.tester; import java.util.Collections; @@ -30,7 +30,10 @@ import org.springframework.core.codec.Encoder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.http.MediaType; import org.springframework.http.codec.CodecConfigurer; +import org.springframework.http.codec.DecoderHttpMessageReader; +import org.springframework.http.codec.EncoderHttpMessageWriter; import org.springframework.lang.Nullable; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; @@ -42,7 +45,10 @@ import org.springframework.util.MimeTypeUtils; * @author Rossen Stoyanchev * @since 1.0.0 */ -public final class CodecMappingProvider implements MappingProvider { +final class EncoderDecoderMappingProvider implements MappingProvider { + + private static final ResolvableType MAP_TYPE = ResolvableType.forClass(Map.class); + private final Encoder> encoder; @@ -54,9 +60,25 @@ public final class CodecMappingProvider implements MappingProvider { * {@link Decoder} in the given {@link CodecConfigurer}. * @throws IllegalArgumentException if there is no JSON encoder or decoder. */ - public CodecMappingProvider(CodecConfigurer configurer) { - this.encoder = CodecDelegate.findJsonEncoder(configurer); - this.decoder = CodecDelegate.findJsonDecoder(configurer); + public EncoderDecoderMappingProvider(CodecConfigurer configurer) { + this.encoder = findJsonEncoder(configurer); + this.decoder = findJsonDecoder(configurer); + } + + private static Decoder> findJsonDecoder(CodecConfigurer configurer) { + return configurer.getReaders().stream() + .filter((reader) -> reader.canRead(MAP_TYPE, MediaType.APPLICATION_JSON)) + .map((reader) -> ((DecoderHttpMessageReader>) reader).getDecoder()) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No JSON Decoder")); + } + + private static Encoder> findJsonEncoder(CodecConfigurer configurer) { + return configurer.getWriters().stream() + .filter((writer) -> writer.canWrite(MAP_TYPE, MediaType.APPLICATION_JSON)) + .map((writer) -> ((EncoderHttpMessageWriter>) writer).getEncoder()) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No JSON Encoder")); } diff --git a/spring-graphql/build.gradle b/spring-graphql/build.gradle index eba8b596..16a15050 100644 --- a/spring-graphql/build.gradle +++ b/spring-graphql/build.gradle @@ -23,7 +23,6 @@ dependencies { compileOnly 'org.jetbrains.kotlin:kotlin-stdlib' compileOnly 'org.jetbrains.kotlinx:kotlinx-coroutines-core' - compileOnly 'com.jayway.jsonpath:json-path' compileOnly 'com.fasterxml.jackson.core:jackson-databind' testImplementation 'org.junit.jupiter:junit-jupiter' diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/AbstractGraphQlClientBuilder.java b/spring-graphql/src/main/java/org/springframework/graphql/client/AbstractGraphQlClientBuilder.java index 8e5238af..ee5d4630 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/AbstractGraphQlClientBuilder.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/AbstractGraphQlClientBuilder.java @@ -17,15 +17,16 @@ package org.springframework.graphql.client; import java.util.function.Consumer; -import java.util.function.Function; - -import com.jayway.jsonpath.Configuration; -import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider; -import com.jayway.jsonpath.spi.mapper.MappingProvider; +import org.springframework.core.codec.Decoder; +import org.springframework.core.codec.Encoder; import org.springframework.graphql.support.CachingDocumentSource; import org.springframework.graphql.support.DocumentSource; import org.springframework.graphql.support.ResourceDocumentSource; +import org.springframework.http.codec.json.Jackson2JsonDecoder; +import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -50,7 +51,11 @@ public abstract class AbstractGraphQlClientBuilder jsonEncoder; + + @Nullable + private Decoder> jsonDecoder; /** @@ -78,11 +83,13 @@ public abstract class AbstractGraphQlClientBuilder configurer) { - this.jsonPathConfig = configurer.apply(this.jsonPathConfig); + protected void setJsonCodecs(Encoder> encoder, Decoder> decoder) { + this.jsonEncoder = encoder; + this.jsonDecoder = decoder; } /** @@ -92,11 +99,12 @@ public abstract class AbstractGraphQlClientBuilder> getBuilderInitializer() { return builder -> { builder.documentSource(documentSource); - builder.configureJsonPathConfig(config -> this.jsonPathConfig); + builder.setJsonCodecs(getJsonEncoder(), getJsonDecoder()); }; } + private Encoder> getJsonEncoder() { + Assert.notNull(this.jsonEncoder, "jsonEncoder has not been set"); + return this.jsonEncoder; + } + + private Decoder> getJsonDecoder() { + Assert.notNull(this.jsonDecoder, "jsonDecoder has not been set"); + return this.jsonDecoder; + } + private static class Jackson2Configurer { - private static final Class> defaultMappingProviderType = - Configuration.defaultConfiguration().mappingProvider().getClass(); + static Encoder> encoder() { + return new Jackson2JsonEncoder(); + } - // We only need a MappingProvider: - // GraphQlTransport returns GraphQlResponse with already parsed JSON - - static Configuration configure(Configuration config) { - MappingProvider provider = config.mappingProvider(); - if (provider == null || defaultMappingProviderType.isInstance(provider)) { - config = config.mappingProvider(new JacksonMappingProvider()); - } - return config; + static Decoder> decoder() { + return new Jackson2JsonDecoder(); } } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/client/ClientGraphQlResponse.java b/spring-graphql/src/main/java/org/springframework/graphql/client/ClientGraphQlResponse.java index 0e0ec886..57138185 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/client/ClientGraphQlResponse.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/client/ClientGraphQlResponse.java @@ -19,38 +19,51 @@ package org.springframework.graphql.client; import org.springframework.core.ParameterizedTypeReference; import org.springframework.graphql.GraphQlResponse; +import org.springframework.lang.Nullable; /** - * {@link GraphQlResponse} for client use with further options to navigate and - * handle the selection set in the response. + * {@link GraphQlResponse} for client use, with further options to handle the + * response. * * @author Rossen Stoyanchev * @since 1.0.0 */ public interface ClientGraphQlResponse extends GraphQlResponse { - /** - * Navigate to the given path under the "data" key of the response map and - * return a representation with further options to decode the field value, - * or to check whether it's valid, and so on. - * @param path relative to the "data" key. - * @return a representation for the field at the given path; this + * Navigate to the given path under the "data" key of the response map where + * the path is a dot-separated string with optional array indexes. + *
Example paths: + *
+ * "hero" + * "hero.name" + * "hero.friends" + * "hero.friends[2]" + * "hero.friends[2].name" + *+ * @param path relative to the "data" key + * @return representation for the field with further options to inspect or + * decode its value; use {@link ResponseField#isValid()} to check if the + * field actually exists and its value is present */ ResponseField field(String path); /** * Decode the full response map to the given target type. * @param type the target class - * @return the decoded value + * @return the decoded value, or {@code null} if the "data" is {@code null} + * @throws FieldAccessException if the response is not {@link #isValid() valid} */ + @Nullable