diff --git a/spring-graphql-docs/src/docs/asciidoc/client.adoc b/spring-graphql-docs/src/docs/asciidoc/client.adoc index 124e0dba..ea5b8480 100644 --- a/spring-graphql-docs/src/docs/asciidoc/client.adoc +++ b/spring-graphql-docs/src/docs/asciidoc/client.adoc @@ -320,7 +320,7 @@ For example, given a file called `projectReleases.graphql` in `src/main/resources/graphql-documents`, with content: [source,graphql,indent=0,subs="verbatim,quotes"] -.src/main/resources/graphql/project.graphql +.src/main/resources/graphql-documents/projectReleases.graphql ---- query projectReleases($slug: ID!) { project(slug: $slug) { @@ -341,7 +341,7 @@ You can then: .retrieve() .toEntity(Project.class); ---- -<1> Load the document from "project.graphql" +<1> Load the document from "projectReleases.graphql" <2> Provide variable values. The "JS GraphQL" plugin for IntelliJ supports GraphQL query files with code completion. diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/GraphQlArgumentBinder.java b/spring-graphql/src/main/java/org/springframework/graphql/data/GraphQlArgumentBinder.java index 5adb3512..ce63da99 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/GraphQlArgumentBinder.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/GraphQlArgumentBinder.java @@ -213,7 +213,7 @@ public class GraphQlArgumentBinder { int i = 0; for (Object rawValue : rawCollection) { segments.push("[" + i++ + "]"); - if (elementClass.isAssignableFrom(rawValue.getClass())) { + if (rawValue == null || elementClass.isAssignableFrom(rawValue.getClass())) { collection.add((T) rawValue); } else if (rawValue instanceof Map) { diff --git a/spring-graphql/src/main/java/org/springframework/graphql/server/webflux/GraphiQlHandler.java b/spring-graphql/src/main/java/org/springframework/graphql/server/webflux/GraphiQlHandler.java index ceec57ea..913fe5f2 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/server/webflux/GraphiQlHandler.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/server/webflux/GraphiQlHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the original author or authors. + * 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. @@ -86,7 +86,7 @@ public class GraphiQlHandler { String wsPathQueryParam = applyContextPath(request, this.graphQlWsPath); builder.queryParam("wsPath", wsPathQueryParam); } - return builder.build(); + return builder.build(request.pathVariables()); } private String applyContextPath(ServerRequest request, String path) { diff --git a/spring-graphql/src/main/java/org/springframework/graphql/server/webmvc/GraphiQlHandler.java b/spring-graphql/src/main/java/org/springframework/graphql/server/webmvc/GraphiQlHandler.java index c07cea0e..fd03317c 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/server/webmvc/GraphiQlHandler.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/server/webmvc/GraphiQlHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the original author or authors. + * 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. @@ -85,7 +85,7 @@ public class GraphiQlHandler { String wsPathQueryParam = applyPathPrefix(request, this.graphQlWsPath); builder.queryParam("wsPath", wsPathQueryParam); } - return builder.build(); + return builder.build(request.pathVariables()); } private String applyPathPrefix(ServerRequest request, String path) { diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/GraphQlArgumentBinderTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/GraphQlArgumentBinderTests.java index b9862ff8..3e1390c2 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/GraphQlArgumentBinderTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/GraphQlArgumentBinderTests.java @@ -16,17 +16,14 @@ package org.springframework.graphql.data; -import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.IntStream; -import java.util.stream.Stream; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -36,9 +33,6 @@ import org.junit.jupiter.api.Test; import org.springframework.core.ResolvableType; import org.springframework.graphql.Book; -import org.springframework.graphql.data.GraphQlArgumentBinder; -import org.springframework.util.ReflectionUtils; -import org.springframework.util.StringUtils; import org.springframework.validation.BindException; import org.springframework.validation.FieldError; @@ -60,7 +54,7 @@ class GraphQlArgumentBinderTests { @Test - void defaultConstructor() throws Exception { + void dataBinding() throws Exception { Object result = this.binder.bind( environment("{\"key\":{\"name\":\"test\"}}"), "key", @@ -71,7 +65,7 @@ class GraphQlArgumentBinderTests { } @Test - void defaultConstructorWithNestedBeanProperty() throws Exception { + void dataBindingWithNestedBeanProperty() throws Exception { Object result = this.binder.bind( environment( @@ -92,7 +86,7 @@ class GraphQlArgumentBinderTests { } @Test - void defaultConstructorWithNestedBeanListProperty() throws Exception { + void dataBindingWithNestedBeanListProperty() throws Exception { Object result = this.binder.bind( environment("{\"key\":{\"items\":[{\"name\":\"first\"},{\"name\":\"second\"}]}}"), "key", @@ -104,7 +98,7 @@ class GraphQlArgumentBinderTests { } @Test // gh-301 - void defaultConstructorWithNestedBeanListEmpty() throws Exception { + void dataBindingWithNestedBeanListEmpty() throws Exception { Object result = this.binder.bind( environment("{\"key\":{\"items\": []}}"), "key", @@ -115,7 +109,7 @@ class GraphQlArgumentBinderTests { } @Test // gh-280 - void defaultConstructorBindingError() { + void dataBindingBindingError() { assertThatThrownBy( () -> this.binder.bind( @@ -130,6 +124,35 @@ class GraphQlArgumentBinderTests { }); } + @Test + @SuppressWarnings("unchecked") + void dataBindingToList() throws Exception { + + Object result = this.binder.bind( + environment("{\"key\": [\"1\", \"2\", \"3\"]}"), "key", + ResolvableType.forClassWithGenerics(List.class, String.class)); + + assertThat(result).isNotNull().isInstanceOf(List.class); + assertThat((List) result).containsExactly("1", "2", "3"); + + // gh-486: List with null element + result = this.binder.bind( + environment("{\"key\": [\"1\", null, \"3\"]}"), "key", + ResolvableType.forClassWithGenerics(List.class, String.class)); + + assertThat(result).isNotNull().isInstanceOf(List.class); + assertThat((List) result).containsExactly("1", null, "3"); + + // Empty list + + result = this.binder.bind( + environment("{\"key\": []}"), "key", + ResolvableType.forClassWithGenerics(List.class, String.class)); + + assertThat(result).isNotNull().isInstanceOf(List.class); + assertThat((List) result).isEmpty(); + } + @Test void primaryConstructor() throws Exception { @@ -261,7 +284,6 @@ class GraphQlArgumentBinderTests { } @Test // gh-392 - @SuppressWarnings("unchecked") void shouldHaveHigherDefaultAutoGrowLimit() throws Exception { String items = IntStream.range(0, 260).mapToObj(value -> "{\"name\":\"test\"}").collect(Collectors.joining(",")); Object result = this.binder.bind( @@ -281,7 +303,6 @@ class GraphQlArgumentBinderTests { assertThat(((ItemSetHolder) result).getItems()).hasSize(5); } - @SuppressWarnings("unchecked") private DataFetchingEnvironment environment(String jsonPayload) throws JsonProcessingException { Map arguments = this.mapper.readValue(jsonPayload, Map.class); diff --git a/spring-graphql/src/test/java/org/springframework/graphql/server/webflux/GraphiQlHandlerTests.java b/spring-graphql/src/test/java/org/springframework/graphql/server/webflux/GraphiQlHandlerTests.java index 6e34b6ff..63b533dd 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/server/webflux/GraphiQlHandlerTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/server/webflux/GraphiQlHandlerTests.java @@ -19,6 +19,7 @@ package org.springframework.graphql.server.webflux; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; +import java.util.Map; import org.junit.jupiter.api.Test; @@ -31,9 +32,11 @@ import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.codec.HttpMessageWriter; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.result.view.ViewResolver; +import org.springframework.web.util.UriComponentsBuilder; import static org.assertj.core.api.Assertions.assertThat; @@ -45,10 +48,13 @@ class GraphiQlHandlerTests { private static final List> MESSAGE_READERS = Collections.emptyList(); - private final GraphiQlHandler handler = new GraphiQlHandler("/graphql", null, - new ByteArrayResource("GRAPHIQL".getBytes(StandardCharsets.UTF_8))); + private final GraphiQlHandler handler = initHandler("/graphql"); + private static GraphiQlHandler initHandler(String path) { + return new GraphiQlHandler(path, null, new ByteArrayResource("GRAPHIQL".getBytes(StandardCharsets.UTF_8))); + } + @Test void shouldRedirectWithPathQueryParameter() { MockServerHttpRequest httpRequest = MockServerHttpRequest.get("/graphiql").build(); @@ -73,6 +79,25 @@ class GraphiQlHandlerTests { assertThat(response.headers().getLocation().toASCIIString()).isEqualTo("/graphiql?path=/graphql&wsPath=/graphql"); } + @Test // gh-478 + void shouldRedirectWithPathVariables() { + Map pathVariables = Collections.singletonMap("envId", "123"); + UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString("/env/{envId}/graphiql"); + String path = uriBuilder.build(pathVariables).toString(); + + MockServerHttpRequest httpRequest = MockServerHttpRequest.get(path).build(); + MockServerWebExchange exchange = MockServerWebExchange.from(httpRequest); + exchange.getAttributes().put(RouterFunctions.URI_TEMPLATE_VARIABLES_ATTRIBUTE, pathVariables); + ServerRequest request = ServerRequest.create(exchange, MESSAGE_READERS); + + GraphiQlHandler graphiQlHandler = initHandler(uriBuilder.build().toString()); + ServerResponse response = graphiQlHandler.handleRequest(request).block(); + + assertThat(response.statusCode()).isEqualTo(HttpStatus.TEMPORARY_REDIRECT); + assertThat(response.headers().getLocation()).isNotNull(); + assertThat(response.headers().getLocation().toASCIIString()).isEqualTo(path + "?path=" + path); + } + @Test void shouldServeGraphiQlHtmlResource() { MockServerHttpRequest httpRequest = MockServerHttpRequest.get("/graphiql").queryParam("path", "/graphql").build(); diff --git a/spring-graphql/src/test/java/org/springframework/graphql/server/webmvc/GraphiQlHandlerTests.java b/spring-graphql/src/test/java/org/springframework/graphql/server/webmvc/GraphiQlHandlerTests.java index 355a8d45..6ea1e37e 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/server/webmvc/GraphiQlHandlerTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/server/webmvc/GraphiQlHandlerTests.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; +import java.util.Map; import jakarta.servlet.ServletException; import jakarta.servlet.http.MappingMatch; @@ -35,9 +36,11 @@ import org.springframework.http.converter.ResourceHttpMessageConverter; import org.springframework.mock.web.MockHttpServletMapping; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.web.servlet.function.RouterFunctions; import org.springframework.web.servlet.function.ServerRequest; import org.springframework.web.servlet.function.ServerResponse; import org.springframework.web.util.ServletRequestPathUtils; +import org.springframework.web.util.UriComponentsBuilder; import static org.assertj.core.api.Assertions.assertThat; @@ -50,8 +53,13 @@ class GraphiQlHandlerTests { private static final List> MESSAGE_READERS = Collections.emptyList(); - private GraphiQlHandler handler = new GraphiQlHandler("/graphql", null, - new ByteArrayResource("GRAPHIQL".getBytes(StandardCharsets.UTF_8))); + private final GraphiQlHandler handler = initHandler("/graphql"); + + + private static GraphiQlHandler initHandler(String path) { + return new GraphiQlHandler(path, null, new ByteArrayResource("GRAPHIQL".getBytes(StandardCharsets.UTF_8))); + } + @Test void shouldRedirectWithPathQueryParameter() { @@ -60,7 +68,8 @@ class GraphiQlHandlerTests { ServerResponse response = this.handler.handleRequest(request); assertThat(response.statusCode()).isEqualTo(HttpStatus.TEMPORARY_REDIRECT); assertThat(response.headers().getLocation()).isNotNull(); - assertThat(response.headers().getLocation().toASCIIString()).isEqualTo("http://localhost/graphiql?path=/graphql"); + assertThat(response.headers().getLocation().toASCIIString()) + .isEqualTo("http://localhost/graphiql?path=/graphql"); } @Test @@ -72,7 +81,27 @@ class GraphiQlHandlerTests { ServerResponse response = wsHandler.handleRequest(request); assertThat(response.statusCode()).isEqualTo(HttpStatus.TEMPORARY_REDIRECT); assertThat(response.headers().getLocation()).isNotNull(); - assertThat(response.headers().getLocation().toASCIIString()).isEqualTo("http://localhost/graphiql?path=/graphql&wsPath=/graphql"); + assertThat(response.headers().getLocation().toASCIIString()) + .isEqualTo("http://localhost/graphiql?path=/graphql&wsPath=/graphql"); + } + + @Test // gh-478 + void shouldRedirectWithPathVariables() { + Map pathVariables = Collections.singletonMap("envId", "123"); + UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString("/env/{envId}/graphiql"); + String path = uriBuilder.build(pathVariables).toString(); + + MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", path); + ServerRequest request = ServerRequest.create(servletRequest, MESSAGE_READERS); + servletRequest.setAttribute(RouterFunctions.URI_TEMPLATE_VARIABLES_ATTRIBUTE, pathVariables); + + GraphiQlHandler graphiQlHandler = initHandler(uriBuilder.build().toString()); + ServerResponse response = graphiQlHandler.handleRequest(request); + + assertThat(response.statusCode()).isEqualTo(HttpStatus.TEMPORARY_REDIRECT); + assertThat(response.headers().getLocation()).isNotNull(); + assertThat(response.headers().getLocation().toASCIIString()) + .isEqualTo("http://localhost" + path + "?path=" + path); } @Test @@ -129,4 +158,4 @@ class GraphiQlHandlerTests { } -} \ No newline at end of file +}