Support for annotated DataFetcher's
Closes gh-61, gh-90
This commit is contained in:
@@ -36,6 +36,7 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.graphql.GraphQlService;
|
||||
import org.springframework.graphql.data.method.AnnotatedDataFetcherRegistrar;
|
||||
import org.springframework.graphql.execution.GraphQlSource;
|
||||
import org.springframework.graphql.web.WebGraphQlHandler;
|
||||
import org.springframework.graphql.web.WebInterceptor;
|
||||
@@ -73,6 +74,18 @@ public class GraphQlWebFluxAutoConfiguration {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(GraphQlWebFluxAutoConfiguration.class);
|
||||
|
||||
@Bean
|
||||
public AnnotatedDataFetcherRegistrar dataFetcherRegistrar(ServerCodecConfigurer configurer) {
|
||||
AnnotatedDataFetcherRegistrar registrar = new AnnotatedDataFetcherRegistrar();
|
||||
registrar.setServerCodecConfigurer(configurer);
|
||||
return registrar;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RuntimeWiringBuilderCustomizer annotatedDataFetcherRuntimeWiringCustomizer(AnnotatedDataFetcherRegistrar registrar) {
|
||||
return registrar::register;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(GraphQlService.class)
|
||||
@ConditionalOnMissingBean
|
||||
|
||||
@@ -40,6 +40,7 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.graphql.GraphQlService;
|
||||
import org.springframework.graphql.data.method.AnnotatedDataFetcherRegistrar;
|
||||
import org.springframework.graphql.execution.GraphQlSource;
|
||||
import org.springframework.graphql.execution.ThreadLocalAccessor;
|
||||
import org.springframework.graphql.web.WebGraphQlHandler;
|
||||
@@ -51,7 +52,7 @@ import org.springframework.graphql.web.webmvc.SchemaHandler;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.GenericHttpMessageConverter;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
import org.springframework.web.servlet.function.RouterFunction;
|
||||
import org.springframework.web.servlet.function.RouterFunctions;
|
||||
@@ -80,6 +81,28 @@ public class GraphQlWebMvcAutoConfiguration {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(GraphQlWebMvcAutoConfiguration.class);
|
||||
|
||||
|
||||
@Bean
|
||||
public AnnotatedDataFetcherRegistrar dataFetcherRegistrar(HttpMessageConverters converters) {
|
||||
AnnotatedDataFetcherRegistrar registrar = new AnnotatedDataFetcherRegistrar();
|
||||
registrar.setJsonMessageConverter(getJsonConverter(converters));
|
||||
return registrar;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static GenericHttpMessageConverter<Object> getJsonConverter(HttpMessageConverters converters) {
|
||||
return converters.getConverters().stream()
|
||||
.filter((candidate) -> candidate.canRead(Map.class, MediaType.APPLICATION_JSON))
|
||||
.findFirst()
|
||||
.map(converter -> (GenericHttpMessageConverter<Object>) converter)
|
||||
.orElseThrow(() -> new IllegalStateException("No JSON converter"));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RuntimeWiringBuilderCustomizer annotatedDataFetcherRuntimeWiringCustomizer(AnnotatedDataFetcherRegistrar registrar) {
|
||||
return registrar::register;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(GraphQlService.class)
|
||||
@ConditionalOnMissingBean
|
||||
@@ -138,12 +161,7 @@ public class GraphQlWebMvcAutoConfiguration {
|
||||
public GraphQlWebSocketHandler graphQlWebSocketHandler(WebGraphQlHandler webGraphQlHandler,
|
||||
GraphQlProperties properties, HttpMessageConverters converters) {
|
||||
|
||||
HttpMessageConverter<?> converter = converters.getConverters().stream()
|
||||
.filter((candidate) -> candidate.canRead(Map.class, MediaType.APPLICATION_JSON))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("No JSON converter"));
|
||||
|
||||
return new GraphQlWebSocketHandler(webGraphQlHandler, converter,
|
||||
return new GraphQlWebSocketHandler(webGraphQlHandler, getJsonConverter(converters),
|
||||
properties.getWebsocket().getConnectionInitTimeout());
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2002-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 io.spring.sample.graphql;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.graphql.data.method.annotation.Argument;
|
||||
import org.springframework.graphql.data.method.annotation.SchemaMapping;
|
||||
import org.springframework.graphql.data.method.annotation.GraphQlController;
|
||||
import org.springframework.graphql.data.method.annotation.MutationMapping;
|
||||
import org.springframework.graphql.data.method.annotation.QueryMapping;
|
||||
|
||||
@GraphQlController
|
||||
public class SalaryController {
|
||||
|
||||
private final EmployeeService employeeService;
|
||||
|
||||
private final SalaryService salaryService;
|
||||
|
||||
public SalaryController(EmployeeService employeeService, SalaryService salaryService) {
|
||||
this.employeeService = employeeService;
|
||||
this.salaryService = salaryService;
|
||||
}
|
||||
|
||||
@QueryMapping
|
||||
public List<Employee> employees() {
|
||||
return this.employeeService.getAllEmployees();
|
||||
}
|
||||
|
||||
@SchemaMapping
|
||||
public Mono<BigDecimal> salary(Employee employee) {
|
||||
return this.salaryService.getSalaryForEmployee(employee);
|
||||
}
|
||||
|
||||
@MutationMapping
|
||||
public void updateSalary(@Argument("input") SalaryInput salaryInput) {
|
||||
String employeeId = salaryInput.getEmployeeId();
|
||||
BigDecimal salary = salaryInput.getNewSalary();
|
||||
this.salaryService.updateSalary(employeeId, salary);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2002-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 io.spring.sample.graphql;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class SalaryInput {
|
||||
|
||||
private String employeeId;
|
||||
|
||||
private BigDecimal newSalary;
|
||||
|
||||
public String getEmployeeId() {
|
||||
return employeeId;
|
||||
}
|
||||
|
||||
public void setEmployeeId(String employeeId) {
|
||||
this.employeeId = employeeId;
|
||||
}
|
||||
|
||||
public BigDecimal getNewSalary() {
|
||||
return newSalary;
|
||||
}
|
||||
|
||||
public void setNewSalary(BigDecimal newSalary) {
|
||||
this.newSalary = newSalary;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-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 io.spring.sample.graphql;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Map;
|
||||
|
||||
import graphql.schema.idl.RuntimeWiring;
|
||||
|
||||
import org.springframework.graphql.boot.RuntimeWiringBuilderCustomizer;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class SampleWiring implements RuntimeWiringBuilderCustomizer {
|
||||
|
||||
final EmployeeService employeeService;
|
||||
|
||||
final SalaryService salaryService;
|
||||
|
||||
public SampleWiring(EmployeeService employeeService, SalaryService salaryService) {
|
||||
this.employeeService = employeeService;
|
||||
this.salaryService = salaryService;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void customize(RuntimeWiring.Builder builder) {
|
||||
builder.type("Query", wiring ->
|
||||
wiring.dataFetcher("employees", env -> this.employeeService.getAllEmployees())
|
||||
);
|
||||
builder.type("Employee", wiring ->
|
||||
wiring.dataFetcher("salary", env -> this.salaryService.getSalaryForEmployee(env.getSource()))
|
||||
);
|
||||
builder.type("Mutation", wiring ->
|
||||
wiring.dataFetcher("updateSalary", env -> {
|
||||
Map<String, String> input = env.getArgument("input");
|
||||
String employeeId = input.get("employeeId");
|
||||
BigDecimal newSalary = new BigDecimal(input.get("salary"));
|
||||
this.salaryService.updateSalary(employeeId, newSalary);
|
||||
return null;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2002-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 io.spring.sample.graphql;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.graphql.data.method.annotation.GraphQlController;
|
||||
import org.springframework.graphql.data.method.annotation.QueryMapping;
|
||||
import org.springframework.graphql.data.method.annotation.SubscriptionMapping;
|
||||
|
||||
@GraphQlController
|
||||
public class SampleController {
|
||||
|
||||
private final DataRepository repository;
|
||||
|
||||
public SampleController(DataRepository dataRepository) {
|
||||
this.repository = dataRepository;
|
||||
}
|
||||
|
||||
@QueryMapping
|
||||
public String greeting() {
|
||||
return this.repository.getBasic();
|
||||
}
|
||||
|
||||
@QueryMapping
|
||||
public Mono<String> greetingMono() {
|
||||
return this.repository.getGreeting();
|
||||
}
|
||||
|
||||
@QueryMapping
|
||||
public Flux<String> greetingsFlux() {
|
||||
return this.repository.getGreetings();
|
||||
}
|
||||
|
||||
@SubscriptionMapping
|
||||
public Flux<String> greetings() {
|
||||
return this.repository.getGreetingsStream();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-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 io.spring.sample.graphql;
|
||||
|
||||
import graphql.schema.idl.RuntimeWiring;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.graphql.boot.RuntimeWiringBuilderCustomizer;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class SampleWiring implements RuntimeWiringBuilderCustomizer {
|
||||
|
||||
private final DataRepository repository;
|
||||
|
||||
public SampleWiring(@Autowired DataRepository dataRepository) {
|
||||
this.repository = dataRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customize(RuntimeWiring.Builder wiringBuilder) {
|
||||
wiringBuilder.type("Query", builder -> builder.dataFetcher("greeting", env -> this.repository.getBasic()));
|
||||
wiringBuilder.type("Query", builder -> builder.dataFetcher("greetingMono", env -> this.repository.getGreeting()));
|
||||
wiringBuilder.type("Query", builder -> builder.dataFetcher("greetingsFlux", env -> this.repository.getGreetings()));
|
||||
wiringBuilder.type("Subscription", builder -> builder.dataFetcher("greetings", env -> this.repository.getGreetingsStream()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2002-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 io.spring.sample.graphql;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.graphql.data.method.annotation.Argument;
|
||||
import org.springframework.graphql.data.method.annotation.SchemaMapping;
|
||||
import org.springframework.graphql.data.method.annotation.GraphQlController;
|
||||
import org.springframework.graphql.data.method.annotation.MutationMapping;
|
||||
import org.springframework.graphql.data.method.annotation.QueryMapping;
|
||||
|
||||
@GraphQlController
|
||||
public class SalaryController {
|
||||
|
||||
private final EmployeeService employeeService;
|
||||
|
||||
private final SalaryService salaryService;
|
||||
|
||||
public SalaryController(EmployeeService employeeService, SalaryService salaryService) {
|
||||
this.employeeService = employeeService;
|
||||
this.salaryService = salaryService;
|
||||
}
|
||||
|
||||
@QueryMapping
|
||||
public List<Employee> employees() {
|
||||
return this.employeeService.getAllEmployees();
|
||||
}
|
||||
|
||||
@SchemaMapping
|
||||
public BigDecimal salary(Employee employee) {
|
||||
return this.salaryService.getSalaryForEmployee(employee);
|
||||
}
|
||||
|
||||
@MutationMapping
|
||||
public void updateSalary(@Argument("input") SalaryInput salaryInput) {
|
||||
String employeeId = salaryInput.getEmployeeId();
|
||||
BigDecimal salary = salaryInput.getNewSalary();
|
||||
this.salaryService.updateSalary(employeeId, salary);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2002-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 io.spring.sample.graphql;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class SalaryInput {
|
||||
|
||||
private String employeeId;
|
||||
|
||||
private BigDecimal newSalary;
|
||||
|
||||
public String getEmployeeId() {
|
||||
return employeeId;
|
||||
}
|
||||
|
||||
public void setEmployeeId(String employeeId) {
|
||||
this.employeeId = employeeId;
|
||||
}
|
||||
|
||||
public BigDecimal getNewSalary() {
|
||||
return newSalary;
|
||||
}
|
||||
|
||||
public void setNewSalary(BigDecimal newSalary) {
|
||||
this.newSalary = newSalary;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package io.spring.sample.graphql;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Map;
|
||||
|
||||
import graphql.schema.idl.RuntimeWiring;
|
||||
|
||||
import org.springframework.graphql.boot.RuntimeWiringBuilderCustomizer;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class SampleWiring implements RuntimeWiringBuilderCustomizer {
|
||||
|
||||
final EmployeeService employeeService;
|
||||
|
||||
final SalaryService salaryService;
|
||||
|
||||
public SampleWiring(EmployeeService employeeService, SalaryService salaryService) {
|
||||
this.employeeService = employeeService;
|
||||
this.salaryService = salaryService;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void customize(RuntimeWiring.Builder builder) {
|
||||
builder.type("Query", wiringBuilder ->
|
||||
wiringBuilder.dataFetcher("employees", env ->
|
||||
employeeService.getAllEmployees()
|
||||
)
|
||||
);
|
||||
builder.type("Employee", wiringBuilder ->
|
||||
wiringBuilder.dataFetcher("salary", env -> {
|
||||
Employee employee = env.getSource();
|
||||
return salaryService.getSalaryForEmployee(employee);
|
||||
})
|
||||
);
|
||||
builder.type("Mutation", wiringBuilder ->
|
||||
wiringBuilder.dataFetcher("updateSalary", env -> {
|
||||
Map<String, String> input = env.getArgument("input");
|
||||
String employeeId = input.get("employeeId");
|
||||
BigDecimal newSalary = new BigDecimal(input.get("salary"));
|
||||
salaryService.updateSalary(employeeId, newSalary);
|
||||
return null;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2002-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 io.spring.sample.graphql.greeting;
|
||||
|
||||
import org.springframework.graphql.data.method.annotation.GraphQlController;
|
||||
import org.springframework.graphql.data.method.annotation.QueryMapping;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
|
||||
import static org.springframework.web.context.request.RequestAttributes.SCOPE_REQUEST;
|
||||
|
||||
@GraphQlController
|
||||
public class GreetingController {
|
||||
|
||||
@QueryMapping
|
||||
public String greeting() {
|
||||
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
|
||||
return "Hello " + attributes.getAttribute(RequestAttributeFilter.NAME_ATTRIBUTE, SCOPE_REQUEST);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package io.spring.sample.graphql.greeting;
|
||||
|
||||
import graphql.schema.idl.RuntimeWiring;
|
||||
|
||||
import org.springframework.graphql.boot.RuntimeWiringBuilderCustomizer;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
|
||||
import static org.springframework.web.context.request.RequestAttributes.SCOPE_REQUEST;
|
||||
|
||||
@Component
|
||||
public class GreetingDataWiring implements RuntimeWiringBuilderCustomizer {
|
||||
|
||||
@Override
|
||||
public void customize(RuntimeWiring.Builder builder) {
|
||||
builder.type("Query", typeWiring -> typeWiring.dataFetcher("greeting", env -> {
|
||||
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
|
||||
return "Hello " + attributes.getAttribute(RequestAttributeFilter.NAME_ATTRIBUTE, SCOPE_REQUEST);
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2002-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 io.spring.sample.graphql.project;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.graphql.data.method.annotation.Argument;
|
||||
import org.springframework.graphql.data.method.annotation.SchemaMapping;
|
||||
import org.springframework.graphql.data.method.annotation.GraphQlController;
|
||||
import org.springframework.graphql.data.method.annotation.QueryMapping;
|
||||
|
||||
@GraphQlController
|
||||
public class ProjectController {
|
||||
|
||||
private final SpringProjectsClient client;
|
||||
|
||||
public ProjectController(SpringProjectsClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@QueryMapping
|
||||
public Project project(@Argument String slug) {
|
||||
return client.fetchProject(slug);
|
||||
}
|
||||
|
||||
@SchemaMapping
|
||||
public List<Release> releases(Project project) {
|
||||
return client.fetchProjectReleases(project.getSlug());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package io.spring.sample.graphql.project;
|
||||
|
||||
import graphql.schema.idl.RuntimeWiring;
|
||||
import org.springframework.graphql.boot.RuntimeWiringBuilderCustomizer;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ProjectDataWiring implements RuntimeWiringBuilderCustomizer {
|
||||
|
||||
private final SpringProjectsClient client;
|
||||
|
||||
public ProjectDataWiring(SpringProjectsClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customize(RuntimeWiring.Builder builder) {
|
||||
builder.type("Query", typeWiring -> typeWiring.dataFetcher("project", env -> {
|
||||
String slug = env.getArgument("slug");
|
||||
return client.fetchProject(slug);
|
||||
})).type("Project", typeWiring -> typeWiring.dataFetcher("releases", env -> {
|
||||
Project project = env.getSource();
|
||||
return client.fetchProjectReleases(project.getSlug());
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2002-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.data.method;
|
||||
|
||||
import graphql.schema.DataFetcher;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import graphql.schema.FieldCoordinates;
|
||||
|
||||
/**
|
||||
* {@link DataFetcher} that wrap and invokes a {@link HandlerMethod}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class AnnotatedDataFetcher implements DataFetcher<Object> {
|
||||
|
||||
private final FieldCoordinates coordinates;
|
||||
|
||||
private final HandlerMethod handlerMethod;
|
||||
|
||||
private final HandlerMethodArgumentResolverComposite argumentResolvers;
|
||||
|
||||
|
||||
public AnnotatedDataFetcher(FieldCoordinates coordinates, HandlerMethod handlerMethod,
|
||||
HandlerMethodArgumentResolverComposite resolvers) {
|
||||
|
||||
this.coordinates = coordinates;
|
||||
this.handlerMethod = handlerMethod;
|
||||
this.argumentResolvers = resolvers;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the {@link FieldCoordinates} the HandlerMethod is mapped to.
|
||||
*/
|
||||
public FieldCoordinates getCoordinates() {
|
||||
return this.coordinates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link HandlerMethod} used to fetch data.
|
||||
*/
|
||||
public HandlerMethod getHandlerMethod() {
|
||||
return this.handlerMethod;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
public Object get(DataFetchingEnvironment environment) throws Exception {
|
||||
|
||||
InvocableHandlerMethod invocable =
|
||||
new InvocableHandlerMethod(this.handlerMethod.createWithResolvedBean(), this.argumentResolvers);
|
||||
|
||||
return invocable.invoke(environment);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
/*
|
||||
* Copyright 2002-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.data.method;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import graphql.schema.DataFetcher;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import graphql.schema.FieldCoordinates;
|
||||
import graphql.schema.idl.RuntimeWiring;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.aop.support.AopUtils;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.core.MethodIntrospector;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.core.codec.Decoder;
|
||||
import org.springframework.core.codec.Encoder;
|
||||
import org.springframework.graphql.data.method.annotation.GraphQlController;
|
||||
import org.springframework.graphql.data.method.annotation.SchemaMapping;
|
||||
import org.springframework.graphql.data.method.annotation.MutationMapping;
|
||||
import org.springframework.graphql.data.method.annotation.QueryMapping;
|
||||
import org.springframework.graphql.data.method.annotation.SubscriptionMapping;
|
||||
import org.springframework.graphql.data.method.annotation.support.InputArgumentMethodArgumentResolver;
|
||||
import org.springframework.graphql.data.method.annotation.support.DataFetchingEnvironmentMethodArgumentResolver;
|
||||
import org.springframework.graphql.data.method.annotation.support.SourceMethodArgumentResolver;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.DecoderHttpMessageReader;
|
||||
import org.springframework.http.codec.EncoderHttpMessageWriter;
|
||||
import org.springframework.http.codec.ServerCodecConfigurer;
|
||||
import org.springframework.http.converter.GenericHttpMessageConverter;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class AnnotatedDataFetcherRegistrar implements ApplicationContextAware, InitializingBean {
|
||||
|
||||
private final static Log logger = LogFactory.getLog(AnnotatedDataFetcherRegistrar.class);
|
||||
|
||||
|
||||
/**
|
||||
* Bean name prefix for target beans behind scoped proxies. Used to exclude those
|
||||
* targets from handler method detection, in favor of the corresponding proxies.
|
||||
* <p>We're not checking the autowire-candidate status here, which is how the
|
||||
* proxy target filtering problem is being handled at the autowiring level,
|
||||
* since autowire-candidate may have been turned to {@code false} for other
|
||||
* reasons, while still expecting the bean to be eligible for handler methods.
|
||||
* <p>Originally defined in {@link org.springframework.aop.scope.ScopedProxyUtils}
|
||||
* but duplicated here to avoid a hard dependency on the spring-aop module.
|
||||
*/
|
||||
private static final String SCOPED_TARGET_NAME_PREFIX = "scopedTarget.";
|
||||
|
||||
private static final ResolvableType MAP_RESOLVABLE_TYPE =
|
||||
ResolvableType.forType(new ParameterizedTypeReference<Map<String, Object>>() {});
|
||||
|
||||
|
||||
@Nullable
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@Nullable
|
||||
private HandlerMethodArgumentResolverComposite argumentResolvers;
|
||||
|
||||
@Nullable
|
||||
private GenericHttpMessageConverter<Object> jsonMessageConverter;
|
||||
|
||||
@Nullable
|
||||
private Encoder<Object> jsonEncoder;
|
||||
|
||||
@Nullable
|
||||
private Decoder<Object> jsonDecoder;
|
||||
|
||||
|
||||
/**
|
||||
* Configure the {@link org.springframework.http.converter.HttpMessageConverter}
|
||||
* to use to convert input arguments obtained from the
|
||||
* {@link DataFetchingEnvironment} and converted to the type of a declared
|
||||
* {@link org.springframework.graphql.data.method.annotation.Argument @Argument}
|
||||
* method parameter.
|
||||
* <p>This method is mutually exclusive with
|
||||
* {@link #setServerCodecConfigurer(ServerCodecConfigurer)} and is convenient
|
||||
* for use in a Spring MVC application but both variant can be used without
|
||||
* much difference.
|
||||
* @param converter the converter to use.
|
||||
*/
|
||||
public void setJsonMessageConverter(@Nullable GenericHttpMessageConverter<Object> converter) {
|
||||
this.jsonMessageConverter = converter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Variant of {@link #setJsonMessageConverter(GenericHttpMessageConverter)}
|
||||
* to use an {@link Encoder} and {@link Decoder} to convert input arguments.
|
||||
* <p>This method is mutually exclusive with
|
||||
* {@link #setJsonMessageConverter(GenericHttpMessageConverter)} and is
|
||||
* convenient for use in a Spring WebFlux application but both variant can
|
||||
* be used without much difference.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public void setServerCodecConfigurer(@Nullable ServerCodecConfigurer configurer) {
|
||||
if (configurer == null) {
|
||||
this.jsonDecoder = null;
|
||||
this.jsonEncoder = null;
|
||||
return;
|
||||
}
|
||||
this.jsonDecoder = configurer.getReaders().stream()
|
||||
.filter((reader) -> reader.canRead(MAP_RESOLVABLE_TYPE, MediaType.APPLICATION_JSON))
|
||||
.map((reader) -> ((DecoderHttpMessageReader<Object>) reader).getDecoder())
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("No Decoder for JSON"));
|
||||
this.jsonEncoder = configurer.getWriters().stream()
|
||||
.filter((writer) -> writer.canWrite(MAP_RESOLVABLE_TYPE, MediaType.APPLICATION_JSON))
|
||||
.map((writer) -> ((EncoderHttpMessageWriter<Object>) writer).getEncoder())
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("No Encoder for JSON"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
this.argumentResolvers = new HandlerMethodArgumentResolverComposite();
|
||||
this.argumentResolvers.addResolver(initInputArgumentMethodArgumentResolver());
|
||||
this.argumentResolvers.addResolver(new DataFetchingEnvironmentMethodArgumentResolver());
|
||||
this.argumentResolvers.addResolver(new SourceMethodArgumentResolver());
|
||||
}
|
||||
|
||||
private InputArgumentMethodArgumentResolver initInputArgumentMethodArgumentResolver() {
|
||||
InputArgumentMethodArgumentResolver argumentResolver;
|
||||
if (this.jsonMessageConverter != null) {
|
||||
argumentResolver = new InputArgumentMethodArgumentResolver(this.jsonMessageConverter);
|
||||
}
|
||||
else if (this.jsonEncoder != null && this.jsonDecoder != null) {
|
||||
argumentResolver = new InputArgumentMethodArgumentResolver(this.jsonDecoder, this.jsonEncoder);
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException(
|
||||
"Neither HttpMessageConverter nor Encoder/Decoder for JSON provided");
|
||||
}
|
||||
return argumentResolver;
|
||||
}
|
||||
|
||||
|
||||
public void register(RuntimeWiring.Builder builder) {
|
||||
Assert.state(this.argumentResolvers != null, "`argumentResolvers` not initialized");
|
||||
Assert.state(this.applicationContext != null, "ApplicationContext is required");
|
||||
|
||||
detectHandlerMethods().forEach((coordinates, handlerMethod) -> {
|
||||
DataFetcher<?> dataFetcher = new AnnotatedDataFetcher(coordinates, handlerMethod, this.argumentResolvers);
|
||||
builder.type(coordinates.getTypeName(), typeBuilder ->
|
||||
typeBuilder.dataFetcher(coordinates.getFieldName(), dataFetcher));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan beans in the ApplicationContext, detect and prepare a map of handler methods.
|
||||
*/
|
||||
private Map<FieldCoordinates, HandlerMethod> detectHandlerMethods() {
|
||||
Map<FieldCoordinates, HandlerMethod> result = new HashMap<>();
|
||||
for (String beanName : this.applicationContext.getBeanNamesForType(Object.class)) {
|
||||
if (beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
|
||||
continue;
|
||||
}
|
||||
Class<?> beanType = null;
|
||||
try {
|
||||
beanType = this.applicationContext.getType(beanName);
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
// An unresolvable bean type, probably from a lazy bean - let's ignore it.
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
|
||||
}
|
||||
}
|
||||
if (beanType == null || !isHandler(beanType)) {
|
||||
continue;
|
||||
}
|
||||
detectHandlerMethodsOnBean(beanName).forEach((coordinates, handlerMethod) -> {
|
||||
HandlerMethod existing = result.put(coordinates, handlerMethod);
|
||||
if (existing != null && !existing.equals(handlerMethod)) {
|
||||
throw new IllegalStateException(
|
||||
"Ambiguous mapping. Cannot map '" + handlerMethod.getBean() + "' method \n" +
|
||||
handlerMethod + "\nto " + coordinates + ": There is already '" +
|
||||
existing.getBean() + "' bean method\n" + existing + " mapped.");
|
||||
}
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean isHandler(Class<?> beanType) {
|
||||
return (AnnotatedElementUtils.hasAnnotation(beanType, GraphQlController.class) ||
|
||||
AnnotatedElementUtils.hasAnnotation(beanType, SchemaMapping.class));
|
||||
}
|
||||
|
||||
private Map<FieldCoordinates, HandlerMethod> detectHandlerMethodsOnBean(Object handler) {
|
||||
Class<?> beanClass = (handler instanceof String ?
|
||||
this.applicationContext.getType((String) handler) : handler.getClass());
|
||||
if (beanClass == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
Class<?> userClass = ClassUtils.getUserClass(beanClass);
|
||||
Map<Method, FieldCoordinates> methodsMap =
|
||||
MethodIntrospector.selectMethods(userClass, (Method method) -> getCoordinates(method, userClass));
|
||||
if (methodsMap.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
Map<FieldCoordinates, HandlerMethod> result = new LinkedHashMap<>(methodsMap.size());
|
||||
for (Map.Entry<Method, FieldCoordinates> entry : methodsMap.entrySet()) {
|
||||
Method method = AopUtils.selectInvocableMethod(entry.getKey(), userClass);
|
||||
HandlerMethod handlerMethod = (handler instanceof String ?
|
||||
new HandlerMethod((String) handler, this.applicationContext.getAutowireCapableBeanFactory(), method) :
|
||||
new HandlerMethod(handler, method));
|
||||
FieldCoordinates coordinates = entry.getValue();
|
||||
coordinates = updateCoordinates(coordinates, handlerMethod);
|
||||
result.put(coordinates, handlerMethod);
|
||||
}
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace(formatMappings(userClass, result));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private FieldCoordinates getCoordinates(Method method, Class<?> handlerType) {
|
||||
QueryMapping query = AnnotatedElementUtils.findMergedAnnotation(method, QueryMapping.class);
|
||||
if (query != null) {
|
||||
String name = (StringUtils.hasText(query.name()) ? query.name() : method.getName());
|
||||
return FieldCoordinates.coordinates("Query", name);
|
||||
}
|
||||
MutationMapping mutation = AnnotatedElementUtils.findMergedAnnotation(method, MutationMapping.class);
|
||||
if (mutation != null) {
|
||||
String name = (StringUtils.hasText(mutation.name()) ? mutation.name() : method.getName());
|
||||
return FieldCoordinates.coordinates("Mutation", name);
|
||||
}
|
||||
SubscriptionMapping subscription = AnnotatedElementUtils.findMergedAnnotation(method, SubscriptionMapping.class);
|
||||
if (subscription != null) {
|
||||
String name = (StringUtils.hasText(subscription.name()) ? subscription.name() : method.getName());
|
||||
return FieldCoordinates.coordinates("Subscription", name);
|
||||
}
|
||||
SchemaMapping schemaMapping = AnnotatedElementUtils.findMergedAnnotation(method, SchemaMapping.class);
|
||||
if (schemaMapping != null) {
|
||||
String typeName = schemaMapping.typeName();
|
||||
String field = schemaMapping.field();
|
||||
if (!StringUtils.hasText(typeName)) {
|
||||
schemaMapping = AnnotatedElementUtils.findMergedAnnotation(handlerType, SchemaMapping.class);
|
||||
if (schemaMapping != null) {
|
||||
typeName = schemaMapping.typeName();
|
||||
}
|
||||
}
|
||||
return FieldCoordinates.coordinates(typeName, field);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private FieldCoordinates updateCoordinates(FieldCoordinates coordinates, HandlerMethod handlerMethod) {
|
||||
boolean hasTypeName = StringUtils.hasText(coordinates.getTypeName());
|
||||
boolean hasFieldName = StringUtils.hasText(coordinates.getFieldName());
|
||||
if (hasTypeName && hasFieldName) {
|
||||
return coordinates;
|
||||
}
|
||||
|
||||
String typeName = coordinates.getTypeName();
|
||||
if (!hasTypeName) {
|
||||
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
|
||||
HandlerMethodArgumentResolver resolver = this.argumentResolvers.getArgumentResolver(parameter);
|
||||
if (resolver instanceof SourceMethodArgumentResolver) {
|
||||
typeName = parameter.getParameterType().getSimpleName();
|
||||
break;
|
||||
}
|
||||
}
|
||||
Assert.hasText(typeName,
|
||||
"No parentType specified, and a source/container method argument was also not found: " +
|
||||
handlerMethod.getShortLogMessage());
|
||||
}
|
||||
|
||||
return FieldCoordinates.coordinates(typeName,
|
||||
(hasFieldName ? coordinates.getFieldName() : handlerMethod.getMethod().getName()));
|
||||
}
|
||||
|
||||
private String formatMappings(Class<?> handlerType, Map<FieldCoordinates, HandlerMethod> mappings) {
|
||||
String formattedType = Arrays.stream(ClassUtils.getPackageName(handlerType).split("\\."))
|
||||
.map(p -> p.substring(0, 1))
|
||||
.collect(Collectors.joining(".", "", "." + handlerType.getSimpleName()));
|
||||
return mappings.entrySet().stream()
|
||||
.map(entry -> {
|
||||
Method method = entry.getValue().getMethod();
|
||||
String methodParameters = Arrays.stream(method.getParameterTypes())
|
||||
.map(Class::getSimpleName)
|
||||
.collect(Collectors.joining(",", "(", ")"));
|
||||
return entry.getKey() + " => " + method.getName() + methodParameters;
|
||||
})
|
||||
.collect(Collectors.joining("\n\t", "\n\t" + formattedType + ":" + "\n\t", ""));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
/*
|
||||
* Copyright 2002-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.data.method;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.core.BridgeMethodResolver;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.core.annotation.SynthesizingMethodParameter;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Encapsulates information about a handler method consisting of a
|
||||
* {@linkplain #getMethod() method} and a {@linkplain #getBean() bean}.
|
||||
* Provides convenient access to method parameters, the method return value,
|
||||
* method annotations, etc.
|
||||
*
|
||||
* <p>The class may be created with a bean instance or with a bean name
|
||||
* (e.g. lazy-init bean, prototype bean). Use {@link #createWithResolvedBean()}
|
||||
* to obtain a {@code HandlerMethod} instance with a bean instance resolved
|
||||
* through the associated {@link BeanFactory}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class HandlerMethod {
|
||||
|
||||
protected static final Log logger = LogFactory.getLog(HandlerMethod.class);
|
||||
|
||||
|
||||
private final Object bean;
|
||||
|
||||
@Nullable
|
||||
private final BeanFactory beanFactory;
|
||||
|
||||
private final Class<?> beanType;
|
||||
|
||||
private final Method method;
|
||||
|
||||
private final Method bridgedMethod;
|
||||
|
||||
private final MethodParameter[] parameters;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor with a handler instance and a method.
|
||||
*/
|
||||
public HandlerMethod(Object bean, Method method) {
|
||||
Assert.notNull(bean, "Bean is required");
|
||||
Assert.notNull(method, "Method is required");
|
||||
this.bean = bean;
|
||||
this.beanFactory = null;
|
||||
this.beanType = ClassUtils.getUserClass(bean);
|
||||
this.method = method;
|
||||
this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
|
||||
this.parameters = initMethodParameters();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with a bean name for the handler along with a {@code BeanFactory}
|
||||
* to allow {@link #createWithResolvedBean() resolving} the handler instance
|
||||
* later.
|
||||
*/
|
||||
public HandlerMethod(String beanName, BeanFactory beanFactory, Method method) {
|
||||
Assert.hasText(beanName, "Bean name is required");
|
||||
Assert.notNull(beanFactory, "BeanFactory is required");
|
||||
Assert.notNull(method, "Method is required");
|
||||
this.bean = beanName;
|
||||
this.beanFactory = beanFactory;
|
||||
Class<?> beanType = beanFactory.getType(beanName);
|
||||
if (beanType == null) {
|
||||
throw new IllegalStateException(
|
||||
"Cannot resolve bean type for bean with name '" + beanName + "'");
|
||||
}
|
||||
this.beanType = ClassUtils.getUserClass(beanType);
|
||||
this.method = method;
|
||||
this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
|
||||
this.parameters = initMethodParameters();
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy constructor for use from subclasses that accept more arguments.
|
||||
*/
|
||||
protected HandlerMethod(HandlerMethod handlerMethod) {
|
||||
this(handlerMethod, handlerMethod.bean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-create HandlerMethod with the resolved handler.
|
||||
*/
|
||||
private HandlerMethod(HandlerMethod handlerMethod, Object handler) {
|
||||
Assert.notNull(handlerMethod, "HandlerMethod is required");
|
||||
Assert.notNull(handler, "Handler object is required");
|
||||
this.bean = handler;
|
||||
this.beanFactory = handlerMethod.beanFactory;
|
||||
this.beanType = handlerMethod.beanType;
|
||||
this.method = handlerMethod.method;
|
||||
this.bridgedMethod = handlerMethod.bridgedMethod;
|
||||
this.parameters = handlerMethod.parameters;
|
||||
}
|
||||
|
||||
private MethodParameter[] initMethodParameters() {
|
||||
int count = this.bridgedMethod.getParameterCount();
|
||||
MethodParameter[] result = new MethodParameter[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
result[i] = new HandlerMethodParameter(i);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the bean for this handler method.
|
||||
*/
|
||||
public Object getBean() {
|
||||
return this.bean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the method for this handler method.
|
||||
*/
|
||||
public Method getMethod() {
|
||||
return this.method;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns the type of the handler for this handler method.
|
||||
* <p>Note that if the bean type is a CGLIB-generated class, the original
|
||||
* user-defined class is returned.
|
||||
*/
|
||||
public Class<?> getBeanType() {
|
||||
return this.beanType;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the bean method is a bridge method, this method returns the bridged
|
||||
* (user-defined) method. Otherwise it returns the same method as {@link #getMethod()}.
|
||||
*/
|
||||
protected Method getBridgedMethod() {
|
||||
return this.bridgedMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the method parameters for this handler method.
|
||||
*/
|
||||
public MethodParameter[] getMethodParameters() {
|
||||
return this.parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the HandlerMethod return type.
|
||||
*/
|
||||
public MethodParameter getReturnType() {
|
||||
return new HandlerMethodParameter(-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the actual return value type.
|
||||
*/
|
||||
public MethodParameter getReturnValueType(@Nullable Object returnValue) {
|
||||
return new ReturnValueMethodParameter(returnValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return {@code true} if the method return type is void, {@code false} otherwise.
|
||||
*/
|
||||
public boolean isVoid() {
|
||||
return Void.TYPE.equals(getReturnType().getParameterType());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a single annotation on the underlying method traversing its super methods
|
||||
* if no annotation can be found on the given method itself.
|
||||
* <p>Also supports <em>merged</em> composed annotations with attribute
|
||||
* overrides as of Spring Framework 4.3.
|
||||
* @param annotationType the type of annotation to introspect the method for
|
||||
* @return the annotation, or {@code null} if none found
|
||||
* @see AnnotatedElementUtils#findMergedAnnotation
|
||||
*/
|
||||
@Nullable
|
||||
public <A extends Annotation> A getMethodAnnotation(Class<A> annotationType) {
|
||||
return AnnotatedElementUtils.findMergedAnnotation(this.method, annotationType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the parameter is declared with the given annotation type.
|
||||
* @param annotationType the annotation type to look for
|
||||
* @see AnnotatedElementUtils#hasAnnotation
|
||||
*/
|
||||
public <A extends Annotation> boolean hasMethodAnnotation(Class<A> annotationType) {
|
||||
return AnnotatedElementUtils.hasAnnotation(this.method, annotationType);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the provided instance contains a bean name rather than an object instance,
|
||||
* the bean name is resolved before a {@link HandlerMethod} is created and returned.
|
||||
*/
|
||||
public HandlerMethod createWithResolvedBean() {
|
||||
Object handler = this.bean;
|
||||
if (this.bean instanceof String) {
|
||||
Assert.state(this.beanFactory != null, "Cannot resolve bean name without BeanFactory");
|
||||
String beanName = (String) this.bean;
|
||||
handler = this.beanFactory.getBean(beanName);
|
||||
}
|
||||
return new HandlerMethod(this, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a short representation of this handler method for log message purposes.
|
||||
*/
|
||||
public String getShortLogMessage() {
|
||||
int args = this.method.getParameterCount();
|
||||
return getBeanType().getSimpleName() + "#" + this.method.getName() + "[" + args + " args]";
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!(other instanceof HandlerMethod)) {
|
||||
return false;
|
||||
}
|
||||
HandlerMethod otherMethod = (HandlerMethod) other;
|
||||
return (this.bean.equals(otherMethod.bean) && this.method.equals(otherMethod.method));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return (this.bean.hashCode() * 31 + this.method.hashCode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.method.toGenericString();
|
||||
}
|
||||
|
||||
|
||||
// Support methods for use in "InvocableHandlerMethod" sub-class variants..
|
||||
|
||||
@Nullable
|
||||
protected static Object findProvidedArgument(MethodParameter parameter, @Nullable Object... providedArgs) {
|
||||
if (!ObjectUtils.isEmpty(providedArgs)) {
|
||||
for (Object providedArg : providedArgs) {
|
||||
if (parameter.getParameterType().isInstance(providedArg)) {
|
||||
return providedArg;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected static String formatArgumentError(MethodParameter param, String message) {
|
||||
return "Could not resolve parameter [" + param.getParameterIndex() + "] in " +
|
||||
param.getExecutable().toGenericString() + (StringUtils.hasText(message) ? ": " + message : "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the target bean class is an instance of the class where the given
|
||||
* method is declared. In some cases the actual endpoint instance at request-
|
||||
* processing time may be a JDK dynamic proxy (lazy initialization, prototype
|
||||
* beans, and others). Endpoint classes that require proxying should prefer
|
||||
* class-based proxy mechanisms.
|
||||
*/
|
||||
protected void assertTargetBean(Method method, Object targetBean, Object[] args) {
|
||||
Class<?> methodDeclaringClass = method.getDeclaringClass();
|
||||
Class<?> targetBeanClass = targetBean.getClass();
|
||||
if (!methodDeclaringClass.isAssignableFrom(targetBeanClass)) {
|
||||
String text = "The mapped handler method class '" + methodDeclaringClass.getName() +
|
||||
"' is not an instance of the actual endpoint bean class '" +
|
||||
targetBeanClass.getName() + "'. If the class requires proxying " +
|
||||
"(e.g. due to @Transactional), please use class-based proxying.";
|
||||
throw new IllegalStateException(formatInvokeError(text, args));
|
||||
}
|
||||
}
|
||||
|
||||
protected String formatInvokeError(String text, Object[] args) {
|
||||
|
||||
String formattedArgs = IntStream.range(0, args.length)
|
||||
.mapToObj(i -> (args[i] != null ?
|
||||
"[" + i + "] [type=" + args[i].getClass().getName() + "] [value=" + args[i] + "]" :
|
||||
"[" + i + "] [null]"))
|
||||
.collect(Collectors.joining(",\n", " ", " "));
|
||||
|
||||
return text + "\n" +
|
||||
"Class [" + getBeanType().getName() + "]\n" +
|
||||
"Method [" + getBridgedMethod().toGenericString() + "] " +
|
||||
"with argument values:\n" + formattedArgs;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A MethodParameter with HandlerMethod-specific behavior.
|
||||
*/
|
||||
protected class HandlerMethodParameter extends SynthesizingMethodParameter {
|
||||
|
||||
public HandlerMethodParameter(int index) {
|
||||
super(HandlerMethod.this.bridgedMethod, index);
|
||||
}
|
||||
|
||||
protected HandlerMethodParameter(HandlerMethodParameter original) {
|
||||
super(original);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getContainingClass() {
|
||||
return HandlerMethod.this.getBeanType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends Annotation> T getMethodAnnotation(Class<T> annotationType) {
|
||||
return HandlerMethod.this.getMethodAnnotation(annotationType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends Annotation> boolean hasMethodAnnotation(Class<T> annotationType) {
|
||||
return HandlerMethod.this.hasMethodAnnotation(annotationType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerMethodParameter clone() {
|
||||
return new HandlerMethodParameter(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A MethodParameter for a HandlerMethod return type based on an actual return value.
|
||||
*/
|
||||
private class ReturnValueMethodParameter extends HandlerMethodParameter {
|
||||
|
||||
@Nullable
|
||||
private final Object returnValue;
|
||||
|
||||
public ReturnValueMethodParameter(@Nullable Object returnValue) {
|
||||
super(-1);
|
||||
this.returnValue = returnValue;
|
||||
}
|
||||
|
||||
protected ReturnValueMethodParameter(ReturnValueMethodParameter original) {
|
||||
super(original);
|
||||
this.returnValue = original.returnValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getParameterType() {
|
||||
return (this.returnValue != null ? this.returnValue.getClass() : super.getParameterType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReturnValueMethodParameter clone() {
|
||||
return new ReturnValueMethodParameter(this);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2002-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.data.method;
|
||||
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
|
||||
/**
|
||||
* Strategy interface for resolving method parameters into argument values in
|
||||
* the context of a given request.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public interface HandlerMethodArgumentResolver {
|
||||
|
||||
/**
|
||||
* Whether the given {@linkplain MethodParameter method parameter} is
|
||||
* supported by this resolver.
|
||||
* @param parameter the method parameter to check
|
||||
* @return {@code true} if this resolver supports the supplied parameter;
|
||||
* {@code false} otherwise
|
||||
*/
|
||||
boolean supportsParameter(MethodParameter parameter);
|
||||
|
||||
/**
|
||||
* Resolves a method parameter into an argument value from a given request.
|
||||
* A {@link ModelAndViewContainer} provides access to the model for the
|
||||
* request. A {@link WebDataBinderFactory} provides a way to create
|
||||
* a {@link WebDataBinder} instance when needed for data binding and
|
||||
* type conversion purposes.
|
||||
* @param parameter the method parameter to resolve. This parameter must
|
||||
* have previously been passed to {@link #supportsParameter} which must
|
||||
* have returned {@code true}.
|
||||
* @param environment the GraphQL {@link DataFetchingEnvironment}
|
||||
* @return the resolved argument value, or {@code null} if not resolvable
|
||||
* @throws Exception in case of errors with the preparation of argument values
|
||||
*/
|
||||
@Nullable
|
||||
Object resolveArgument(MethodParameter parameter, DataFetchingEnvironment environment) throws Exception;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2002-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.data.method;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Resolves method parameters by delegating to a list of registered
|
||||
* {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolver}'s.
|
||||
* Previously resolved method parameters are cached for faster lookups.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver {
|
||||
|
||||
private final List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<>();
|
||||
|
||||
private final Map<MethodParameter, HandlerMethodArgumentResolver> argumentResolverCache =
|
||||
new ConcurrentHashMap<>(256);
|
||||
|
||||
|
||||
/**
|
||||
* Add the given {@link HandlerMethodArgumentResolver}.
|
||||
*/
|
||||
public void addResolver(HandlerMethodArgumentResolver resolver) {
|
||||
this.argumentResolvers.add(resolver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a read-only list with the contained resolvers, or an empty list.
|
||||
*/
|
||||
public List<HandlerMethodArgumentResolver> getResolvers() {
|
||||
return Collections.unmodifiableList(this.argumentResolvers);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Whether the given {@linkplain MethodParameter method parameter} is
|
||||
* supported by any registered {@link HandlerMethodArgumentResolver}.
|
||||
*/
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return getArgumentResolver(parameter) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over registered
|
||||
* {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}
|
||||
* and invoke the one that supports it.
|
||||
* @throws IllegalArgumentException if no suitable argument resolver is found
|
||||
*/
|
||||
@Override
|
||||
@Nullable
|
||||
public Object resolveArgument(MethodParameter parameter, DataFetchingEnvironment environment) throws Exception {
|
||||
HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
|
||||
if (resolver == null) {
|
||||
throw new IllegalArgumentException("Unsupported parameter type [" +
|
||||
parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
|
||||
}
|
||||
return resolver.resolveArgument(parameter, environment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a registered {@link HandlerMethodArgumentResolver} that supports
|
||||
* the given method parameter.
|
||||
*/
|
||||
@Nullable
|
||||
public HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
|
||||
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
|
||||
if (result == null) {
|
||||
for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
|
||||
if (resolver.supportsParameter(parameter)) {
|
||||
result = resolver;
|
||||
this.argumentResolverCache.put(parameter, result);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright 2002-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.data.method;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
|
||||
import org.springframework.core.CoroutinesUtils;
|
||||
import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||
import org.springframework.core.KotlinDetector;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Extension of {@link HandlerMethod} that can resolve method arguments from a
|
||||
* {@link DataFetchingEnvironment} and invoke the method.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class InvocableHandlerMethod extends HandlerMethod {
|
||||
|
||||
private static final Object[] EMPTY_ARGS = new Object[0];
|
||||
|
||||
|
||||
private final HandlerMethodArgumentResolverComposite resolvers;
|
||||
|
||||
private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
|
||||
|
||||
|
||||
public InvocableHandlerMethod(HandlerMethod handlerMethod, HandlerMethodArgumentResolverComposite resolvers) {
|
||||
super(handlerMethod);
|
||||
Assert.isTrue(!resolvers.getResolvers().isEmpty(), "No argument resolvers");
|
||||
this.resolvers = resolvers;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the configured argument resolvers.
|
||||
*/
|
||||
public HandlerMethodArgumentResolverComposite getResolvers() {
|
||||
return this.resolvers;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Invoke the method after resolving its argument values in the context of
|
||||
* the given environment.
|
||||
* <p>Argument values are commonly resolved through
|
||||
* {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}.
|
||||
* The {@code providedArgs} parameter however may supply argument values to
|
||||
* be used directly, i.e. without argument resolution. Provided argument
|
||||
* values are checked before argument resolvers.
|
||||
* @param environment the GraphQL {@link DataFetchingEnvironment}
|
||||
* @return the raw value returned by the invoked method
|
||||
* @throws Exception raised if no suitable argument resolver can be found,
|
||||
* or if the method raised an exception
|
||||
* @see #getMethodArgumentValues
|
||||
* @see #doInvoke
|
||||
*/
|
||||
@Nullable
|
||||
public Object invoke(DataFetchingEnvironment environment) throws Exception {
|
||||
Object[] args = getMethodArgumentValues(environment);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Arguments: " + Arrays.toString(args));
|
||||
}
|
||||
return doInvoke(args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the method argument values for the current request, checking the provided
|
||||
* argument values and falling back to the configured argument resolvers.
|
||||
* <p>The resulting array will be passed into {@link #doInvoke}.
|
||||
*/
|
||||
protected Object[] getMethodArgumentValues(
|
||||
DataFetchingEnvironment environment, Object... providedArgs) throws Exception {
|
||||
|
||||
MethodParameter[] parameters = getMethodParameters();
|
||||
if (ObjectUtils.isEmpty(parameters)) {
|
||||
return EMPTY_ARGS;
|
||||
}
|
||||
Object[] args = new Object[parameters.length];
|
||||
for (int i = 0; i < parameters.length; i++) {
|
||||
MethodParameter parameter = parameters[i];
|
||||
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
|
||||
args[i] = findProvidedArgument(parameter, providedArgs);
|
||||
if (args[i] != null) {
|
||||
continue;
|
||||
}
|
||||
if (!this.resolvers.supportsParameter(parameter)) {
|
||||
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
|
||||
}
|
||||
try {
|
||||
args[i] = this.resolvers.resolveArgument(parameter, environment);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// Leave stack trace for later, exception may actually be resolved and handled...
|
||||
if (logger.isDebugEnabled()) {
|
||||
String exMsg = ex.getMessage();
|
||||
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
|
||||
logger.debug(formatArgumentError(parameter, exMsg));
|
||||
}
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the handler method with the given argument values.
|
||||
*/
|
||||
@Nullable
|
||||
protected Object doInvoke(Object... args) throws Exception {
|
||||
Method method = getBridgedMethod();
|
||||
ReflectionUtils.makeAccessible(method);
|
||||
try {
|
||||
if (KotlinDetector.isSuspendingFunction(method)) {
|
||||
return CoroutinesUtils.invokeSuspendingFunction(method, getBean(), args);
|
||||
}
|
||||
return method.invoke(getBean(), args);
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
assertTargetBean(method, getBean(), args);
|
||||
String text = (ex.getMessage() != null ? ex.getMessage() : "Illegal argument");
|
||||
throw new IllegalStateException(formatInvokeError(text, args), ex);
|
||||
}
|
||||
catch (InvocationTargetException ex) {
|
||||
// Unwrap for DataFetcherExceptionResolvers ...
|
||||
Throwable targetException = ex.getTargetException();
|
||||
if (targetException instanceof RuntimeException) {
|
||||
throw (RuntimeException) targetException;
|
||||
}
|
||||
else if (targetException instanceof Error) {
|
||||
throw (Error) targetException;
|
||||
}
|
||||
else if (targetException instanceof Exception) {
|
||||
throw (Exception) targetException;
|
||||
}
|
||||
else {
|
||||
throw new IllegalStateException(formatInvokeError("Invocation failure", args), targetException);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2002-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.data.method.annotation;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
|
||||
/**
|
||||
* Annotation to bind a method parameter to a GraphQL input
|
||||
* {@link graphql.schema.DataFetchingEnvironment#getArgument(String) argument}.
|
||||
*
|
||||
* <p>If the method parameter is {@link java.util.Map Map<String, Object>} or
|
||||
* and a parameter name is not specified, then the map parameter is populated
|
||||
* via {@link graphql.schema.DataFetchingEnvironment#getArguments()}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Target(ElementType.PARAMETER)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface Argument {
|
||||
|
||||
/**
|
||||
* Alias for {@link #name}.
|
||||
*/
|
||||
@AliasFor("name")
|
||||
String value() default "";
|
||||
|
||||
/**
|
||||
* The name of the input argument to bind to.
|
||||
*/
|
||||
@AliasFor("value")
|
||||
String name() default "";
|
||||
|
||||
/**
|
||||
* Whether the input argument is required.
|
||||
* <p>Defaults to {@code true}, leading to an exception being thrown
|
||||
* if the argument is missing. Switch this to {@code false} if you prefer
|
||||
* a {@code null} value when the parameter is not present.
|
||||
* <p>Alternatively, provide a {@link #defaultValue}, which implicitly
|
||||
* sets this flag to {@code false}.
|
||||
*/
|
||||
boolean required() default true;
|
||||
|
||||
/**
|
||||
* The default value to use as a fallback when an input argument is
|
||||
* not present or has an empty value.
|
||||
* <p>Supplying a default value implicitly sets {@link #required} to
|
||||
* {@code false}.
|
||||
*/
|
||||
String defaultValue() default ValueConstants.DEFAULT_NONE;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2002-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.data.method.annotation;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
/**
|
||||
* Indicates the annotated class is a "Controller" in a GraphQL application and
|
||||
* exposes handler methods that fetch data, typically annotated with
|
||||
* {@link SchemaMapping} annotations, or one of its shortcut annotations
|
||||
* {@link QueryMapping}, {@link MutationMapping}, and {@link SubscriptionMapping}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Controller
|
||||
public @interface GraphQlController {
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2002-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.data.method.annotation;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
|
||||
/**
|
||||
* Annotation to express a mapping to a GraphQL Mutation operation.
|
||||
*
|
||||
* <p>Specifically, {@code @QueryMapping} is a <em>composed annotation</em> that
|
||||
* acts as a shortcut for {@code @SchemaMapping(typeName = "Mutation")}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@SchemaMapping(typeName = "Mutation")
|
||||
public @interface MutationMapping {
|
||||
|
||||
/**
|
||||
* Alias for {@link SchemaMapping#field()}.
|
||||
*/
|
||||
@AliasFor(annotation = SchemaMapping.class, attribute = "field")
|
||||
String name() default "";
|
||||
|
||||
/**
|
||||
* Alias for {@link SchemaMapping#field()}.
|
||||
*/
|
||||
@AliasFor(annotation = SchemaMapping.class, attribute = "field")
|
||||
String value() default "";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2002-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.data.method.annotation;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
|
||||
/**
|
||||
* Annotation to express a mapping to a GraphQL Query operation.
|
||||
*
|
||||
* <p>Specifically, {@code @QueryMapping} is a <em>composed annotation</em> that
|
||||
* acts as a shortcut for {@code @SchemaMapping(typeName = "Query")}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Target(value = {ElementType.TYPE, ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@SchemaMapping(typeName = "Query")
|
||||
public @interface QueryMapping {
|
||||
|
||||
/**
|
||||
* Alias for {@link SchemaMapping#field()}.
|
||||
*/
|
||||
@AliasFor(annotation = SchemaMapping.class, attribute = "field")
|
||||
String name() default "";
|
||||
|
||||
/**
|
||||
* Alias for {@link SchemaMapping#field()}.
|
||||
*/
|
||||
@AliasFor(annotation = SchemaMapping.class, attribute = "field")
|
||||
String value() default "";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2002-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.data.method.annotation;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
|
||||
/**
|
||||
* Annotation to express a mapping to a GraphQL type and field pair. Typically
|
||||
* used on methods, but also possible to use at the class level to specify a
|
||||
* default type name for all handler methods.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface SchemaMapping {
|
||||
|
||||
/**
|
||||
* Customize the name of the GraphQL field to bind to.
|
||||
* <p>By default, if not specified, this is initialized from the method name.
|
||||
*/
|
||||
@AliasFor("value")
|
||||
String field() default "";
|
||||
|
||||
/**
|
||||
* Effectively an alias for {@link #field()}.
|
||||
*/
|
||||
@AliasFor("field")
|
||||
String value() default "";
|
||||
|
||||
/**
|
||||
* Customizes the name of the parent/container type for the GraphQL field.
|
||||
* <p>By default, if not specified, it is derived from the class name of a
|
||||
* {@link DataFetchingEnvironment#getSource() source} argument injected into
|
||||
* the handler method.
|
||||
* <p>This attributed is supported at the class level and at the method level!
|
||||
* When used on both levels, the one on the method level overrides the one
|
||||
* at the class level.
|
||||
*/
|
||||
String typeName() default "";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2002-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.data.method.annotation;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
|
||||
/**
|
||||
* Annotation to express a mapping to a GraphQL Subscription operation.
|
||||
*
|
||||
* <p>Specifically, {@code @QueryMapping} is a <em>composed annotation</em> that
|
||||
* acts as a shortcut for {@code @SchemaMapping(typeName = "Subscription")}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@SchemaMapping(typeName = "Subscription")
|
||||
public @interface SubscriptionMapping {
|
||||
|
||||
/**
|
||||
* Alias for {@link SchemaMapping#field()}.
|
||||
*/
|
||||
@AliasFor(annotation = SchemaMapping.class, attribute = "field")
|
||||
String name() default "";
|
||||
|
||||
/**
|
||||
* Alias for {@link SchemaMapping#field()}.
|
||||
*/
|
||||
@AliasFor(annotation = SchemaMapping.class, attribute = "field")
|
||||
String value() default "";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2002-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.data.method.annotation;
|
||||
|
||||
/**
|
||||
* Common annotation value constants.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public interface ValueConstants {
|
||||
|
||||
/**
|
||||
* Constant defining a value for no default - as a replacement for {@code null} which
|
||||
* we cannot use in annotation attributes.
|
||||
* <p>This is an artificial, fixed value of 16 unicode characters, with its sole purpose
|
||||
* being to never match a user-declared value.
|
||||
* @see Argument#defaultValue()
|
||||
*/
|
||||
String DEFAULT_NONE = "\n\t\t\n\t\t\n\uE000\uE001\uE002\n\t\t\t\t\n";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Annotations for binding data fetching methods to GraphQL schema queries,
|
||||
* mutations, subscriptions, and fields.
|
||||
*/
|
||||
@NonNullApi
|
||||
@NonNullFields
|
||||
package org.springframework.graphql.data.method.annotation;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
import org.springframework.lang.NonNullFields;
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2002-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.data.method.annotation.support;
|
||||
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
|
||||
|
||||
/**
|
||||
* Resolver for a {@link DataFetchingEnvironment} argument.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class DataFetchingEnvironmentMethodArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return parameter.getParameterType().equals(DataFetchingEnvironment.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object resolveArgument(MethodParameter parameter, DataFetchingEnvironment environment) {
|
||||
return environment;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
* Copyright 2002-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.data.method.annotation.support;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.codec.Decoder;
|
||||
import org.springframework.core.codec.Encoder;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
|
||||
import org.springframework.graphql.data.method.annotation.Argument;
|
||||
import org.springframework.graphql.data.method.annotation.ValueConstants;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.HttpOutputMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.GenericHttpMessageConverter;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Resolver for {@link Argument @Argument} annotated method parameters, obtained
|
||||
* via {@link DataFetchingEnvironment#getArgument(String)} and converted to the
|
||||
* declared type of the method parameter.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class InputArgumentMethodArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
private final ArgumentConverter argumentConverter;
|
||||
|
||||
/**
|
||||
* Constructor with an
|
||||
* {@link org.springframework.http.converter.HttpMessageConverter} to convert
|
||||
* Map-based input arguments to higher level Objects.
|
||||
*/
|
||||
public InputArgumentMethodArgumentResolver(GenericHttpMessageConverter<Object> converter) {
|
||||
this.argumentConverter = new MessageConverterArgumentConverter(converter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Variant of
|
||||
* {@link #InputArgumentMethodArgumentResolver(GenericHttpMessageConverter)}
|
||||
* to use an {@link Encoder} and {@link Decoder} to convert input arguments.
|
||||
*/
|
||||
public InputArgumentMethodArgumentResolver(Decoder<Object> decoder, Encoder<Object> encoder) {
|
||||
this.argumentConverter = new CodecArgumentConverter(decoder, encoder);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return parameter.getParameterAnnotation(Argument.class) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object resolveArgument(MethodParameter parameter, DataFetchingEnvironment environment) throws Exception {
|
||||
Argument annotation = parameter.getParameterAnnotation(Argument.class);
|
||||
Assert.notNull(annotation, "No @Argument annotation");
|
||||
String name = annotation.name();
|
||||
if (!StringUtils.hasText(name)) {
|
||||
name = parameter.getParameterName();
|
||||
if (name == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Name for argument of type [" + parameter.getNestedParameterType().getName() +
|
||||
"] not specified, and parameter name information not found in class file either.");
|
||||
}
|
||||
}
|
||||
|
||||
Object rawValue = (ValueConstants.DEFAULT_NONE.equals(annotation.defaultValue()) ?
|
||||
environment.getArgument(name) :
|
||||
environment.getArgumentOrDefault(name, annotation.defaultValue()));
|
||||
|
||||
Class<?> parameterType = parameter.getParameterType();
|
||||
|
||||
if (rawValue == null) {
|
||||
if (annotation.required()) {
|
||||
throw new MissingArgumentException(name, parameter);
|
||||
}
|
||||
if (parameterType.equals(Optional.class)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (parameterType.isAssignableFrom(rawValue.getClass())) {
|
||||
return returnValue(rawValue, parameterType);
|
||||
}
|
||||
|
||||
if (rawValue instanceof List) {
|
||||
Assert.isAssignable(List.class, parameterType,
|
||||
"Argument '" + name + "' is a List while the @Argument method parameter is " + parameterType);
|
||||
List<?> valueList = (List<?>) rawValue;
|
||||
Class<?> elementType = parameter.nestedIfOptional().getNestedParameterType();
|
||||
if (valueList.isEmpty() || elementType.isAssignableFrom(valueList.get(0).getClass())) {
|
||||
return returnValue(rawValue, parameterType);
|
||||
}
|
||||
}
|
||||
|
||||
Object decodedValue = this.argumentConverter.convert(rawValue, parameter);
|
||||
Assert.notNull(decodedValue, "Argument '" + name + "' with raw value '" + rawValue + "'was decoded to null");
|
||||
return returnValue(decodedValue, parameterType);
|
||||
}
|
||||
|
||||
private Object returnValue(Object value, Class<?> parameterType) {
|
||||
return (parameterType.equals(Optional.class) ? Optional.of(value) : value);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Contract to abstract use of an HttpMessageConverter vs Encoder/Decoder.
|
||||
*/
|
||||
private interface ArgumentConverter {
|
||||
|
||||
@Nullable
|
||||
Object convert(Object rawValue, MethodParameter targetParameter) throws Exception;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* HttpMessageConverter based implementation of ArgumentConverter.
|
||||
*/
|
||||
private static class MessageConverterArgumentConverter implements ArgumentConverter {
|
||||
|
||||
private final GenericHttpMessageConverter<Object> converter;
|
||||
|
||||
public MessageConverterArgumentConverter(GenericHttpMessageConverter<Object> converter) {
|
||||
this.converter = converter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convert(Object rawValue, MethodParameter targetParameter) throws IOException {
|
||||
HttpOutputMessageAdapter outMessage = new HttpOutputMessageAdapter();
|
||||
this.converter.write(rawValue, MediaType.APPLICATION_JSON, outMessage);
|
||||
|
||||
HttpInputMessageAdapter inMessage = new HttpInputMessageAdapter(outMessage);
|
||||
return this.converter.read(targetParameter.getGenericParameterType(), rawValue.getClass(), inMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Encoder/Decoder based implementation of ArgumentConverter.
|
||||
*/
|
||||
private static class CodecArgumentConverter implements ArgumentConverter {
|
||||
|
||||
private final Decoder<Object> decoder;
|
||||
|
||||
private final Encoder<Object> encoder;
|
||||
|
||||
public CodecArgumentConverter(Decoder<Object> decoder, Encoder<Object> encoder) {
|
||||
Assert.notNull(decoder, "Decoder is required");
|
||||
Assert.notNull(encoder, "Encoder is required");
|
||||
this.decoder = decoder;
|
||||
this.encoder = encoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convert(Object rawValue, MethodParameter targetParameter) {
|
||||
DataBuffer dataBuffer = this.encoder.encodeValue(
|
||||
rawValue, DefaultDataBufferFactory.sharedInstance, ResolvableType.forInstance(rawValue),
|
||||
MimeTypeUtils.APPLICATION_JSON, Collections.emptyMap());
|
||||
|
||||
return this.decoder.decode(
|
||||
dataBuffer, ResolvableType.forMethodParameter(targetParameter.nestedIfOptional()),
|
||||
MimeTypeUtils.APPLICATION_JSON, Collections.emptyMap());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class HttpInputMessageAdapter extends ByteArrayInputStream implements HttpInputMessage {
|
||||
|
||||
HttpInputMessageAdapter(HttpOutputMessageAdapter messageAdapter) {
|
||||
super(messageAdapter.toByteArray());
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getBody() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders getHeaders() {
|
||||
return HttpHeaders.EMPTY;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class HttpOutputMessageAdapter extends ByteArrayOutputStream implements HttpOutputMessage {
|
||||
|
||||
private static final HttpHeaders noOpHeaders = new HttpHeaders();
|
||||
|
||||
@Override
|
||||
public OutputStream getBody() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders getHeaders() {
|
||||
return noOpHeaders;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2002-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.data.method.annotation.support;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.NestedRuntimeException;
|
||||
|
||||
/**
|
||||
* Indicates that an input argument value in the method parameters of an
|
||||
* annotated DataFetcher method is not present.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class MissingArgumentException extends NestedRuntimeException {
|
||||
|
||||
private final String argumentName;
|
||||
|
||||
private final MethodParameter parameter;
|
||||
|
||||
|
||||
public MissingArgumentException(String argumentName, MethodParameter parameter) {
|
||||
super("");
|
||||
this.argumentName = argumentName;
|
||||
this.parameter = parameter;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the expected name of the input argument.
|
||||
*/
|
||||
public String getArgumentName() {
|
||||
return this.argumentName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the method parameter bound to the input argument.
|
||||
*/
|
||||
public MethodParameter getParameter() {
|
||||
return this.parameter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "Required argument '" + this.argumentName +"' for method parameter type " +
|
||||
this.parameter.getNestedParameterType().getSimpleName() + " is not present";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2002-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.data.method.annotation.support;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Resolver for parent/container of a field, obtained via
|
||||
* {@link DataFetchingEnvironment#getSource()}.
|
||||
*
|
||||
* <p>This resolver supports any non-simple value type, also excluding arrays
|
||||
* and collections, and therefore must be ordered last, in a fallback mode,
|
||||
* allowing other resolvers to resolve the argument first.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class SourceMethodArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
Class<?> type = parameter.getParameterType();
|
||||
return (!BeanUtils.isSimpleValueType(type) && !type.isArray() && !Collection.class.isAssignableFrom(type));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object resolveArgument(MethodParameter parameter, DataFetchingEnvironment environment) {
|
||||
Object source = environment.getSource();
|
||||
Assert.isInstanceOf(parameter.getParameterType(), source,
|
||||
"The declared parameter of type '" + parameter.getParameterType() + "' " +
|
||||
"does not match the type of the source Object '" + source.getClass() + "'.");
|
||||
return source;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Resolvers for method parameters of annotated handler methods.
|
||||
*/
|
||||
|
||||
@NonNullApi
|
||||
@NonNullFields
|
||||
package org.springframework.graphql.data.method.annotation.support;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
import org.springframework.lang.NonNullFields;
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Support for DataFetcher's based on methods in a handler class.
|
||||
*/
|
||||
|
||||
@NonNullApi
|
||||
@NonNullFields
|
||||
package org.springframework.graphql.data.method;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
import org.springframework.lang.NonNullFields;
|
||||
@@ -17,6 +17,8 @@ package org.springframework.graphql;
|
||||
|
||||
public class Author {
|
||||
|
||||
Long id;
|
||||
|
||||
String firstName;
|
||||
|
||||
String lastName;
|
||||
@@ -24,12 +26,21 @@ public class Author {
|
||||
public Author() {
|
||||
}
|
||||
|
||||
public Author(String firstName, String lastName) {
|
||||
public Author(Long id, String firstName, String lastName) {
|
||||
this.id = id;
|
||||
this.firstName = firstName;
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return this.firstName;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2002-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;
|
||||
|
||||
public class BookCriteria {
|
||||
|
||||
private Long id;
|
||||
|
||||
private String author;
|
||||
|
||||
|
||||
public Long getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getAuthor() {
|
||||
return this.author;
|
||||
}
|
||||
|
||||
public void setAuthor(String author) {
|
||||
this.author = author;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,18 +20,29 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
public class BookSource {
|
||||
|
||||
private static final Map<Long, Book> booksMap = new HashMap<>(4);
|
||||
private static final Map<Long, Book> booksMap = new HashMap<>();
|
||||
|
||||
private static final Map<Long, Author> authorsMap = new HashMap<>();
|
||||
|
||||
static {
|
||||
booksMap.put(1L, new Book(1L, "Nineteen Eighty-Four", new Author("George", "Orwell")));
|
||||
booksMap.put(2L, new Book(2L, "The Great Gatsby", new Author("F. Scott", "Fitzgerald")));
|
||||
booksMap.put(3L, new Book(3L, "Catch-22", new Author("Joseph", "Heller")));
|
||||
booksMap.put(4L, new Book(4L, "To The Lighthouse", new Author("Virginia", "Woolf")));
|
||||
booksMap.put(5L, new Book(5L, "Animal Farm", new Author("George", "Orwell")));
|
||||
booksMap.put(42L, new Book(42L, "Hitchhiker's Guide to the Galaxy", new Author("Douglas", "Adams")));
|
||||
booksMap.put(53L, new Book(53L, "Breaking Bad", new Author("Vince", "Gilligan")));
|
||||
authorsMap.put(1L, new Author(1L, "George", "Orwell"));
|
||||
authorsMap.put(2L, new Author(2L, "F. Scott", "Fitzgerald"));
|
||||
authorsMap.put(3L, new Author(3L, "Joseph", "Heller"));
|
||||
authorsMap.put(4L, new Author(4L, "Virginia", "Woolf"));
|
||||
authorsMap.put(5L, new Author(5L, "Douglas", "Adams"));
|
||||
authorsMap.put(6L, new Author(6L, "Vince", "Gilligan"));
|
||||
|
||||
booksMap.put(1L, new Book(1L, "Nineteen Eighty-Four", authorsMap.get(1L)));
|
||||
booksMap.put(2L, new Book(2L, "The Great Gatsby", authorsMap.get(2L)));
|
||||
booksMap.put(3L, new Book(3L, "Catch-22", authorsMap.get(3L)));
|
||||
booksMap.put(4L, new Book(4L, "To The Lighthouse", authorsMap.get(4L)));
|
||||
booksMap.put(5L, new Book(5L, "Animal Farm", authorsMap.get(1L)));
|
||||
booksMap.put(42L, new Book(42L, "Hitchhiker's Guide to the Galaxy", authorsMap.get(5L)));
|
||||
booksMap.put(53L, new Book(53L, "Breaking Bad", authorsMap.get(6L)));
|
||||
}
|
||||
|
||||
|
||||
@@ -47,4 +58,16 @@ public class BookSource {
|
||||
return booksMap.get(id);
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
public static List<Book> findBooksByAuthor(String author) {
|
||||
return Flux.fromIterable(books())
|
||||
.filter((book) -> book.getAuthor().getFullName().contains(author))
|
||||
.collectList()
|
||||
.block();
|
||||
}
|
||||
|
||||
public static Author getAuthor(Long id) {
|
||||
return authorsMap.get(id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
/*
|
||||
* Copyright 2002-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.data.method;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import graphql.ExecutionResult;
|
||||
import graphql.GraphQL;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.graphql.Author;
|
||||
import org.springframework.graphql.Book;
|
||||
import org.springframework.graphql.BookCriteria;
|
||||
import org.springframework.graphql.BookSource;
|
||||
import org.springframework.graphql.data.method.annotation.Argument;
|
||||
import org.springframework.graphql.data.method.annotation.GraphQlController;
|
||||
import org.springframework.graphql.data.method.annotation.MutationMapping;
|
||||
import org.springframework.graphql.data.method.annotation.QueryMapping;
|
||||
import org.springframework.graphql.data.method.annotation.SchemaMapping;
|
||||
import org.springframework.graphql.data.method.annotation.SubscriptionMapping;
|
||||
import org.springframework.graphql.execution.GraphQlSource;
|
||||
import org.springframework.http.codec.ServerCodecConfigurer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests with invocation of DataFetcher's from annotated methods.
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class AnnotatedDataFetcherInvocationTests {
|
||||
|
||||
@Test
|
||||
void queryWithScalarArgument() {
|
||||
String query = "{ " +
|
||||
" bookById(id:\"1\") { " +
|
||||
" id" +
|
||||
" name" +
|
||||
" author {" +
|
||||
" firstName" +
|
||||
" lastName" +
|
||||
" }" +
|
||||
" }" +
|
||||
"}";
|
||||
|
||||
ExecutionResult result = initGraphQl(BookController.class).execute(query);
|
||||
|
||||
assertThat(result.getErrors()).isEmpty();
|
||||
Map<String, Object> data = result.getData();
|
||||
assertThat(data).isNotNull();
|
||||
|
||||
Map<String, Object> book = getValue(data, "bookById");
|
||||
assertThat(book.get("id")).isEqualTo("1");
|
||||
assertThat(book.get("name")).isEqualTo("Nineteen Eighty-Four");
|
||||
|
||||
Map<String, Object> author = getValue(book, "author");
|
||||
assertThat(author.get("firstName")).isEqualTo("George");
|
||||
assertThat(author.get("lastName")).isEqualTo("Orwell");
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryWithObjectArgument() {
|
||||
String query = "{ " +
|
||||
" booksByCriteria(criteria: {author:\"Orwell\"}) { " +
|
||||
" id" +
|
||||
" name" +
|
||||
" }" +
|
||||
"}";
|
||||
|
||||
ExecutionResult result = initGraphQl(BookController.class).execute(query);
|
||||
|
||||
assertThat(result.getErrors()).isEmpty();
|
||||
Map<String, Object> data = result.getData();
|
||||
assertThat(data).isNotNull();
|
||||
|
||||
List<Map<String, Object>> bookList = getValue(data, "booksByCriteria");
|
||||
assertThat(bookList).hasSize(2);
|
||||
assertThat(bookList.get(0).get("name")).isEqualTo("Nineteen Eighty-Four");
|
||||
assertThat(bookList.get(1).get("name")).isEqualTo("Animal Farm");
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryWithArgumentViaDataFetchingEnvironment() {
|
||||
String query = "{ " +
|
||||
" authorById(id:\"1\") { " +
|
||||
" id" +
|
||||
" firstName" +
|
||||
" lastName" +
|
||||
" }" +
|
||||
"}";
|
||||
|
||||
ExecutionResult result = initGraphQl(BookController.class).execute(query);
|
||||
|
||||
assertThat(result.getErrors()).isEmpty();
|
||||
Map<String, Object> data = result.getData();
|
||||
assertThat(data).isNotNull();
|
||||
|
||||
Map<String, Object> author = getValue(data, "authorById");
|
||||
assertThat(author.get("id")).isEqualTo("1");
|
||||
assertThat(author.get("firstName")).isEqualTo("George");
|
||||
assertThat(author.get("lastName")).isEqualTo("Orwell");
|
||||
}
|
||||
|
||||
@Test
|
||||
void mutation() {
|
||||
String operation = "mutation { " +
|
||||
" addAuthor(firstName:\"James\", lastName:\"Joyce\") { " +
|
||||
" id" +
|
||||
" firstName" +
|
||||
" lastName" +
|
||||
" }" +
|
||||
"}";
|
||||
|
||||
ExecutionResult result = initGraphQl(BookController.class).execute(operation);
|
||||
|
||||
assertThat(result.getErrors()).isEmpty();
|
||||
Map<String, Object> data = result.getData();
|
||||
assertThat(data).isNotNull();
|
||||
|
||||
Map<String, Object> author = getValue(data, "addAuthor");
|
||||
assertThat(author.get("id")).isEqualTo("99");
|
||||
assertThat(author.get("firstName")).isEqualTo("James");
|
||||
assertThat(author.get("lastName")).isEqualTo("Joyce");
|
||||
}
|
||||
|
||||
@Test
|
||||
void subscription() {
|
||||
String operation = "subscription { " +
|
||||
" bookSearch(author:\"Orwell\") { " +
|
||||
" id" +
|
||||
" name" +
|
||||
" }" +
|
||||
"}";
|
||||
|
||||
ExecutionResult result = initGraphQl(BookController.class).execute(operation);
|
||||
|
||||
assertThat(result.getErrors()).isEmpty();
|
||||
Publisher<ExecutionResult> publisher = result.getData();
|
||||
assertThat(publisher).isNotNull();
|
||||
|
||||
Flux<Map<String, Object>> bookFlux = Flux.from(publisher).map(rs -> {
|
||||
Map<String, Object> map = rs.getData();
|
||||
return (Map<String, Object>) map.get("bookSearch");
|
||||
});
|
||||
|
||||
StepVerifier.create(bookFlux)
|
||||
.consumeNextWith(book -> {
|
||||
assertThat(book.get("id")).isEqualTo("1");
|
||||
assertThat(book.get("name")).isEqualTo("Nineteen Eighty-Four");
|
||||
})
|
||||
.consumeNextWith(book -> {
|
||||
assertThat(book.get("id")).isEqualTo("5");
|
||||
assertThat(book.get("name")).isEqualTo("Animal Farm");
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
|
||||
private GraphQL initGraphQl(Class<?> beanClass) {
|
||||
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
|
||||
applicationContext.registerBean(beanClass);
|
||||
applicationContext.refresh();
|
||||
|
||||
AnnotatedDataFetcherRegistrar registrar = new AnnotatedDataFetcherRegistrar();
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
registrar.setServerCodecConfigurer(ServerCodecConfigurer.create());
|
||||
registrar.afterPropertiesSet();
|
||||
|
||||
GraphQlSource graphQlSource = GraphQlSource.builder()
|
||||
.schemaResources(new ClassPathResource("books/schema.graphqls"))
|
||||
.configureRuntimeWiring(registrar::register)
|
||||
.build();
|
||||
|
||||
return graphQlSource.graphQl();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> T getValue(Map<String, Object> data, String key) {
|
||||
return (T) data.get(key);
|
||||
}
|
||||
|
||||
|
||||
@GraphQlController
|
||||
private static class BookController {
|
||||
|
||||
@QueryMapping
|
||||
public Book bookById(@Argument Long id) {
|
||||
return new Book(id, BookSource.getBook(id).getName(), null);
|
||||
}
|
||||
|
||||
@QueryMapping
|
||||
public List<Book> booksByCriteria(@Argument BookCriteria criteria) {
|
||||
return BookSource.findBooksByAuthor(criteria.getAuthor());
|
||||
}
|
||||
|
||||
@SchemaMapping
|
||||
public Author author(Book book) {
|
||||
return BookSource.getBook(book.getId()).getAuthor();
|
||||
}
|
||||
|
||||
@QueryMapping
|
||||
public Author authorById(DataFetchingEnvironment environment) {
|
||||
String id = environment.getArgument("id");
|
||||
return BookSource.getAuthor(Long.parseLong(id));
|
||||
}
|
||||
|
||||
@MutationMapping
|
||||
public Author addAuthor(@Argument String firstName, @Argument String lastName) {
|
||||
return new Author(99L, firstName, lastName);
|
||||
}
|
||||
|
||||
@SubscriptionMapping
|
||||
public Flux<Book> bookSearch(@Argument String author) {
|
||||
return Flux.fromIterable(BookSource.findBooksByAuthor(author));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright 2002-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.data.method;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import graphql.schema.DataFetcher;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import graphql.schema.idl.RuntimeWiring;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.graphql.Author;
|
||||
import org.springframework.graphql.Book;
|
||||
import org.springframework.graphql.data.method.annotation.Argument;
|
||||
import org.springframework.graphql.data.method.annotation.GraphQlController;
|
||||
import org.springframework.graphql.data.method.annotation.SchemaMapping;
|
||||
import org.springframework.graphql.data.method.annotation.MutationMapping;
|
||||
import org.springframework.graphql.data.method.annotation.QueryMapping;
|
||||
import org.springframework.graphql.data.method.annotation.SubscriptionMapping;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link AnnotatedDataFetcherRegistrar}.
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class AnnotatedDataFetcherRegistrarTests {
|
||||
|
||||
@Test
|
||||
void registerWithDefaultCoordinates() {
|
||||
RuntimeWiring.Builder wiringBuilder = initRuntimeWiringBuilder(BookController.class);
|
||||
|
||||
Map<String, Map<String, DataFetcher>> fetcherMap = wiringBuilder.build().getDataFetchers();
|
||||
assertThat(fetcherMap).containsOnlyKeys("Query", "Mutation", "Subscription", "Book");
|
||||
assertThat(fetcherMap.get("Query")).containsOnlyKeys("bookById", "bookByIdCustomized");
|
||||
assertThat(fetcherMap.get("Mutation")).containsOnlyKeys("saveBook", "saveBookCustomized");
|
||||
assertThat(fetcherMap.get("Subscription")).containsOnlyKeys("bookSearch", "bookSearchCustomized");
|
||||
assertThat(fetcherMap.get("Book")).containsOnlyKeys("author", "authorCustomized");
|
||||
|
||||
checkMappedMethod(fetcherMap, "Query", "bookById", "bookById");
|
||||
checkMappedMethod(fetcherMap, "Mutation", "saveBook", "saveBook");
|
||||
checkMappedMethod(fetcherMap, "Subscription", "bookSearch", "bookSearch");
|
||||
checkMappedMethod(fetcherMap, "Book", "author", "author");
|
||||
}
|
||||
|
||||
@Test
|
||||
void registerWithExplicitCoordinates() {
|
||||
RuntimeWiring.Builder wiringBuilder = initRuntimeWiringBuilder(BookController.class);
|
||||
|
||||
Map<String, Map<String, DataFetcher>> fetcherMap = wiringBuilder.build().getDataFetchers();
|
||||
assertThat(fetcherMap).containsOnlyKeys("Query", "Mutation", "Subscription", "Book");
|
||||
assertThat(fetcherMap.get("Query")).containsOnlyKeys("bookById", "bookByIdCustomized");
|
||||
assertThat(fetcherMap.get("Mutation")).containsOnlyKeys("saveBook", "saveBookCustomized");
|
||||
assertThat(fetcherMap.get("Subscription")).containsOnlyKeys("bookSearch", "bookSearchCustomized");
|
||||
assertThat(fetcherMap.get("Book")).containsOnlyKeys("author", "authorCustomized");
|
||||
|
||||
checkMappedMethod(fetcherMap, "Query", "bookByIdCustomized", "bookByIdWithNonMatchingMethodName");
|
||||
checkMappedMethod(fetcherMap, "Mutation", "saveBookCustomized", "saveBookWithNonMatchingMethodName");
|
||||
checkMappedMethod(fetcherMap, "Subscription", "bookSearchCustomized", "bookSearchWithNonMatchingMethodName");
|
||||
checkMappedMethod(fetcherMap, "Book", "authorCustomized", "authorWithNonMatchingMethodName");
|
||||
}
|
||||
|
||||
private RuntimeWiring.Builder initRuntimeWiringBuilder(Class<?> handlerType) {
|
||||
AnnotationConfigApplicationContext appContext = new AnnotationConfigApplicationContext();
|
||||
appContext.registerBean(handlerType);
|
||||
appContext.refresh();
|
||||
|
||||
AnnotatedDataFetcherRegistrar registrar = new AnnotatedDataFetcherRegistrar();
|
||||
registrar.setJsonMessageConverter(new MappingJackson2HttpMessageConverter());
|
||||
registrar.setApplicationContext(appContext);
|
||||
registrar.afterPropertiesSet();
|
||||
|
||||
RuntimeWiring.Builder wiringBuilder = RuntimeWiring.newRuntimeWiring();
|
||||
registrar.register(wiringBuilder);
|
||||
return wiringBuilder;
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private void checkMappedMethod(
|
||||
Map<String, Map<String, DataFetcher>> fetcherMap, String typeName, String fieldName, String methodName) {
|
||||
|
||||
AnnotatedDataFetcher fetcher = (AnnotatedDataFetcher) fetcherMap.get(typeName).get(fieldName);
|
||||
assertThat(fetcher.getHandlerMethod().getMethod().getName()).isEqualTo(methodName);
|
||||
}
|
||||
|
||||
|
||||
@GraphQlController
|
||||
private static class BookController {
|
||||
|
||||
|
||||
@QueryMapping
|
||||
public Book bookById(@Argument String id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@MutationMapping
|
||||
public void saveBook(Book book) {
|
||||
}
|
||||
|
||||
@SubscriptionMapping
|
||||
public Flux<Book> bookSearch(@Argument String author) {
|
||||
return Flux.empty();
|
||||
}
|
||||
|
||||
@SchemaMapping
|
||||
public Author author(DataFetchingEnvironment environment, Book book) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Field name explicitly specified
|
||||
|
||||
@QueryMapping("bookByIdCustomized")
|
||||
public Book bookByIdWithNonMatchingMethodName(@Argument String id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@MutationMapping("saveBookCustomized")
|
||||
public void saveBookWithNonMatchingMethodName(Book book) {
|
||||
}
|
||||
|
||||
@SubscriptionMapping("bookSearchCustomized")
|
||||
public Flux<Book> bookSearchWithNonMatchingMethodName(@Argument String author) {
|
||||
return Flux.empty();
|
||||
}
|
||||
|
||||
@SchemaMapping("authorCustomized")
|
||||
public Author authorWithNonMatchingMethodName(Book book) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,21 @@
|
||||
type Query {
|
||||
bookById(id: ID): Book
|
||||
books(id: ID, name: String, author: String): [Book]
|
||||
booksByCriteria(criteria:BookCriteria): [Book]
|
||||
authorById(id: ID): Author
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
addAuthor(firstName: String, lastName: String): Author
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
bookSearch(author: String) : Book!
|
||||
}
|
||||
|
||||
input BookCriteria {
|
||||
id: ID
|
||||
author: String
|
||||
}
|
||||
|
||||
type Book {
|
||||
@@ -10,10 +25,7 @@ type Book {
|
||||
}
|
||||
|
||||
type Author {
|
||||
id: ID
|
||||
firstName: String
|
||||
lastName: String
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
bookSearch(author: String) : Book!
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<Logger name="org.springframework" level="debug" />
|
||||
<Logger name="org.springframework.graphql" level="trace" />
|
||||
<Root level="error">
|
||||
<AppenderRef ref="Console" />
|
||||
</Root>
|
||||
|
||||
Reference in New Issue
Block a user