Support for annotated DataFetcher's

Closes gh-61, gh-90
This commit is contained in:
Rossen Stoyanchev
2021-08-04 16:35:57 +01:00
parent c2a996e564
commit 0d0b84dc8a
41 changed files with 2783 additions and 217 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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&lt;String, Object&gt;} 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;
}

View File

@@ -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 {
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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!
}

View File

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