Extract base classes for annotated controller methods
See gh-864
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -17,12 +17,9 @@ package org.springframework.graphql.data.method.annotation.support;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -32,7 +29,6 @@ import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import graphql.execution.DataFetcherResult;
|
||||
import graphql.schema.DataFetcher;
|
||||
@@ -40,29 +36,19 @@ import graphql.schema.DataFetchingEnvironment;
|
||||
import graphql.schema.FieldCoordinates;
|
||||
import graphql.schema.GraphQLCodeRegistry;
|
||||
import graphql.schema.idl.RuntimeWiring;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.dataloader.DataLoader;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.aop.support.AopUtils;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.context.expression.BeanFactoryResolver;
|
||||
import org.springframework.core.KotlinDetector;
|
||||
import org.springframework.core.MethodIntrospector;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.data.domain.ScrollPosition;
|
||||
import org.springframework.format.FormatterRegistrar;
|
||||
import org.springframework.format.support.DefaultFormattingConversionService;
|
||||
import org.springframework.format.support.FormattingConversionService;
|
||||
import org.springframework.graphql.data.GraphQlArgumentBinder;
|
||||
import org.springframework.graphql.data.method.HandlerMethod;
|
||||
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
|
||||
@@ -83,14 +69,11 @@ import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.validation.DataBinder;
|
||||
|
||||
|
||||
/**
|
||||
* {@link RuntimeWiringConfigurer} that finds {@link SchemaMapping @SchemaMapping}
|
||||
* annotated handler methods in {@link Controller @Controller} classes declared in
|
||||
* Spring configuration, and registers them as {@link DataFetcher}s.
|
||||
*
|
||||
* <p>In addition to initializing a {@link RuntimeWiring.Builder}, this class, also
|
||||
* provides an option to {@link #configure(GraphQLCodeRegistry.Builder) configure}
|
||||
* data fetchers on a {@link GraphQLCodeRegistry.Builder}.
|
||||
* and {@link BatchMapping @BatchMapping} methods in {@link Controller @Controller}
|
||||
* classes, and registers them as {@link DataFetcher}s.
|
||||
*
|
||||
* <p>This class detects the following strategies in Spring configuration,
|
||||
* expecting to find a single, unique bean of that type:
|
||||
@@ -103,13 +86,17 @@ import org.springframework.validation.DataBinder;
|
||||
* is configured for use.
|
||||
* </ul>
|
||||
*
|
||||
*
|
||||
* <p>In addition to initializing a {@link RuntimeWiring.Builder}, this class, also
|
||||
* provides an option to {@link #configure(GraphQLCodeRegistry.Builder) configure}
|
||||
* data fetchers on a {@link GraphQLCodeRegistry.Builder}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class AnnotatedControllerConfigurer implements ApplicationContextAware, InitializingBean, RuntimeWiringConfigurer {
|
||||
public class AnnotatedControllerConfigurer
|
||||
extends AnnotatedControllerDetectionSupport<DataFetcherMappingInfo>
|
||||
implements RuntimeWiringConfigurer {
|
||||
|
||||
private static final ClassLoader classLoader = AnnotatedControllerConfigurer.class.getClassLoader();
|
||||
|
||||
@@ -123,63 +110,14 @@ public class AnnotatedControllerConfigurer implements ApplicationContextAware, I
|
||||
"jakarta.validation.executable.ExecutableValidator", classLoader);
|
||||
|
||||
|
||||
private final static Log logger = LogFactory.getLog(AnnotatedControllerConfigurer.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 final FormattingConversionService conversionService = new DefaultFormattingConversionService();
|
||||
|
||||
private boolean fallBackOnDirectFieldAccess;
|
||||
|
||||
private final List<HandlerMethodArgumentResolver> customArgumentResolvers = new ArrayList<>(8);
|
||||
|
||||
@Nullable
|
||||
private HandlerMethodArgumentResolverComposite argumentResolvers;
|
||||
|
||||
@Nullable
|
||||
private ValidationHelper validationHelper;
|
||||
|
||||
@Nullable
|
||||
private AnnotatedControllerExceptionResolver exceptionResolver;
|
||||
|
||||
@Nullable
|
||||
private Executor executor;
|
||||
|
||||
@Nullable
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
|
||||
/**
|
||||
* Add a {@code FormatterRegistrar} to customize the {@link ConversionService}
|
||||
* that assists in binding GraphQL arguments onto
|
||||
* {@link org.springframework.graphql.data.method.annotation.Argument @Argument}
|
||||
* annotated method parameters.
|
||||
*/
|
||||
public void addFormatterRegistrar(FormatterRegistrar registrar) {
|
||||
registrar.registerFormatters(this.conversionService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether binding GraphQL arguments onto
|
||||
* {@link org.springframework.graphql.data.method.annotation.Argument @Argument}
|
||||
* should falls back to direct field access in case the target object does
|
||||
* not use accessor methods.
|
||||
* @since 1.2.0
|
||||
*/
|
||||
public void setFallBackOnDirectFieldAccess(boolean fallBackOnDirectFieldAccess) {
|
||||
this.fallBackOnDirectFieldAccess = fallBackOnDirectFieldAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a {@link HandlerMethodArgumentResolver} for custom controller method
|
||||
@@ -193,12 +131,6 @@ public class AnnotatedControllerConfigurer implements ApplicationContextAware, I
|
||||
this.customArgumentResolvers.add(resolver);
|
||||
}
|
||||
|
||||
HandlerMethodArgumentResolverComposite getArgumentResolvers() {
|
||||
Assert.notNull(this.argumentResolvers,
|
||||
"HandlerMethodArgumentResolverComposite is not yet initialized, was afterPropertiesSet called?");
|
||||
return this.argumentResolvers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link DataFetcherExceptionResolver} that resolves exceptions with
|
||||
* {@code @GraphQlExceptionHandler} methods in {@code @ControllerAdvice}
|
||||
@@ -230,31 +162,14 @@ public class AnnotatedControllerConfigurer implements ApplicationContextAware, I
|
||||
public void setDataBinderInitializer(@Nullable Consumer<DataBinder> consumer) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure an {@link Executor} to use for asynchronous handling of
|
||||
* {@link Callable} return values from controller methods.
|
||||
* <p>By default, this is not set in which case controller methods with a
|
||||
* {@code Callable} return value cannot be registered.
|
||||
* @param executor the executor to use
|
||||
*/
|
||||
public void setExecutor(Executor executor) {
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
super.afterPropertiesSet();
|
||||
|
||||
this.argumentResolvers = initArgumentResolvers();
|
||||
|
||||
this.exceptionResolver = new AnnotatedControllerExceptionResolver(this.argumentResolvers);
|
||||
if (this.applicationContext != null) {
|
||||
this.exceptionResolver.registerControllerAdvice(this.applicationContext);
|
||||
this.exceptionResolver = new AnnotatedControllerExceptionResolver(getArgumentResolvers());
|
||||
if (getApplicationContext() != null) {
|
||||
this.exceptionResolver.registerControllerAdvice(getApplicationContext());
|
||||
}
|
||||
|
||||
if (beanValidationPresent) {
|
||||
@@ -262,7 +177,8 @@ public class AnnotatedControllerConfigurer implements ApplicationContextAware, I
|
||||
}
|
||||
}
|
||||
|
||||
private HandlerMethodArgumentResolverComposite initArgumentResolvers() {
|
||||
@Override
|
||||
protected HandlerMethodArgumentResolverComposite initArgumentResolvers() {
|
||||
|
||||
HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite();
|
||||
|
||||
@@ -273,7 +189,7 @@ public class AnnotatedControllerConfigurer implements ApplicationContextAware, I
|
||||
}
|
||||
|
||||
GraphQlArgumentBinder argumentBinder =
|
||||
new GraphQlArgumentBinder(this.conversionService, this.fallBackOnDirectFieldAccess);
|
||||
new GraphQlArgumentBinder(getConversionService(), isFallBackOnDirectFieldAccess());
|
||||
|
||||
resolvers.addResolver(new ArgumentMethodArgumentResolver(argumentBinder));
|
||||
resolvers.addResolver(new ArgumentsMethodArgumentResolver(argumentBinder));
|
||||
@@ -332,90 +248,28 @@ public class AnnotatedControllerConfigurer implements ApplicationContextAware, I
|
||||
}
|
||||
}
|
||||
|
||||
protected final ApplicationContext obtainApplicationContext() {
|
||||
Assert.state(this.applicationContext != null, "No ApplicationContext");
|
||||
return this.applicationContext;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void configure(RuntimeWiring.Builder runtimeWiringBuilder) {
|
||||
Assert.state(this.argumentResolvers != null, "`argumentResolvers` is not initialized");
|
||||
Assert.state(this.exceptionResolver != null, "`exceptionResolver` is not initialized");
|
||||
|
||||
findHandlerMethods().forEach((info) -> {
|
||||
detectHandlerMethods().forEach(info -> {
|
||||
DataFetcher<?> dataFetcher;
|
||||
if (!info.isBatchMapping()) {
|
||||
dataFetcher = new SchemaMappingDataFetcher(
|
||||
info, this.argumentResolvers, this.validationHelper, this.exceptionResolver, this.executor);
|
||||
info, getArgumentResolvers(), this.validationHelper, this.exceptionResolver, getExecutor());
|
||||
}
|
||||
else {
|
||||
dataFetcher = registerBatchLoader(info);
|
||||
}
|
||||
runtimeWiringBuilder.type(info.getCoordinates().getTypeName(), typeBuilder ->
|
||||
typeBuilder.dataFetcher(info.getCoordinates().getFieldName(), dataFetcher));
|
||||
FieldCoordinates coordinates = info.getCoordinates();
|
||||
runtimeWiringBuilder.type(coordinates.getTypeName(), typeBuilder ->
|
||||
typeBuilder.dataFetcher(coordinates.getFieldName(), dataFetcher));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan beans in the ApplicationContext, detect and prepare a map of handler methods.
|
||||
*/
|
||||
private Collection<MappingInfo> findHandlerMethods() {
|
||||
ApplicationContext context = obtainApplicationContext();
|
||||
Map<FieldCoordinates, MappingInfo> result = new HashMap<>();
|
||||
for (String beanName : context.getBeanNamesForType(Object.class)) {
|
||||
if (beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
|
||||
continue;
|
||||
}
|
||||
Class<?> beanType = null;
|
||||
try {
|
||||
beanType = context.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 || !AnnotatedElementUtils.hasAnnotation(beanType, Controller.class)) {
|
||||
continue;
|
||||
}
|
||||
Class<?> beanClass = context.getType(beanName);
|
||||
findHandlerMethods(beanName, beanClass).forEach((info) -> {
|
||||
HandlerMethod handlerMethod = info.getHandlerMethod();
|
||||
MappingInfo existing = result.put(info.getCoordinates(), info);
|
||||
if (existing != null && !existing.getHandlerMethod().equals(handlerMethod)) {
|
||||
throw new IllegalStateException(
|
||||
"Ambiguous mapping. Cannot map '" + handlerMethod.getBean() + "' method \n" +
|
||||
handlerMethod + "\nto " + info.getCoordinates() + ": There is already '" +
|
||||
existing.getHandlerMethod().getBean() + "' bean method\n" + existing + " mapped.");
|
||||
}
|
||||
});
|
||||
}
|
||||
return result.values();
|
||||
}
|
||||
|
||||
private Collection<MappingInfo> findHandlerMethods(Object handler, @Nullable Class<?> handlerClass) {
|
||||
if (handlerClass == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Class<?> userClass = ClassUtils.getUserClass(handlerClass);
|
||||
Map<Method, MappingInfo> map = MethodIntrospector.selectMethods(
|
||||
userClass, (Method method) -> getMappingInfo(method, handler, userClass));
|
||||
|
||||
Collection<MappingInfo> mappingInfos = map.values();
|
||||
|
||||
if (logger.isTraceEnabled() && !mappingInfos.isEmpty()) {
|
||||
logger.trace(formatMappings(userClass, mappingInfos));
|
||||
}
|
||||
|
||||
return mappingInfos;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private MappingInfo getMappingInfo(Method method, Object handler, Class<?> handlerType) {
|
||||
|
||||
@Override
|
||||
protected DataFetcherMappingInfo getMappingInfo(Method method, Object handler, Class<?> handlerType) {
|
||||
Set<Annotation> annotations = AnnotatedElementUtils.findAllMergedAnnotations(
|
||||
method, new LinkedHashSet<>(Arrays.asList(BatchMapping.class, SchemaMapping.class)));
|
||||
|
||||
@@ -457,8 +311,7 @@ public class AnnotatedControllerConfigurer implements ApplicationContextAware, I
|
||||
if (!StringUtils.hasText(typeName)) {
|
||||
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
|
||||
if (!batchMapping) {
|
||||
Assert.state(this.argumentResolvers != null, "`argumentResolvers` is not initialized");
|
||||
HandlerMethodArgumentResolver resolver = this.argumentResolvers.getArgumentResolver(parameter);
|
||||
HandlerMethodArgumentResolver resolver = getArgumentResolvers().getArgumentResolver(parameter);
|
||||
if (resolver instanceof SourceMethodArgumentResolver) {
|
||||
typeName = parameter.getParameterType().getSimpleName();
|
||||
break;
|
||||
@@ -477,32 +330,15 @@ public class AnnotatedControllerConfigurer implements ApplicationContextAware, I
|
||||
"No parentType specified, and a source/parent method argument was also not found: " +
|
||||
handlerMethod.getShortLogMessage());
|
||||
|
||||
return new MappingInfo(typeName, field, batchMapping, batchSize, handlerMethod);
|
||||
return new DataFetcherMappingInfo(typeName, field, batchMapping, batchSize, handlerMethod);
|
||||
}
|
||||
|
||||
private HandlerMethod createHandlerMethod(Method method, Object handler, Class<?> handlerType) {
|
||||
Method theMethod = AopUtils.selectInvocableMethod(method, handlerType);
|
||||
return (handler instanceof String ?
|
||||
new HandlerMethod((String) handler, obtainApplicationContext().getAutowireCapableBeanFactory(), theMethod) :
|
||||
new HandlerMethod(handler, theMethod));
|
||||
@Override
|
||||
protected HandlerMethod getHandlerMethod(DataFetcherMappingInfo mappingInfo) {
|
||||
return mappingInfo.getHandlerMethod();
|
||||
}
|
||||
|
||||
private String formatMappings(Class<?> handlerType, Collection<MappingInfo> infos) {
|
||||
String formattedType = Arrays.stream(ClassUtils.getPackageName(handlerType).split("\\."))
|
||||
.map(p -> p.substring(0, 1))
|
||||
.collect(Collectors.joining(".", "", "." + handlerType.getSimpleName()));
|
||||
return infos.stream()
|
||||
.map(mappingInfo -> {
|
||||
Method method = mappingInfo.getHandlerMethod().getMethod();
|
||||
String methodParameters = Arrays.stream(method.getGenericParameterTypes())
|
||||
.map(Type::getTypeName)
|
||||
.collect(Collectors.joining(",", "(", ")"));
|
||||
return mappingInfo.getCoordinates() + " => " + method.getName() + methodParameters;
|
||||
})
|
||||
.collect(Collectors.joining("\n\t", "\n\t" + formattedType + ":" + "\n\t", ""));
|
||||
}
|
||||
|
||||
private DataFetcher<Object> registerBatchLoader(MappingInfo info) {
|
||||
private DataFetcher<Object> registerBatchLoader(DataFetcherMappingInfo info) {
|
||||
if (!info.isBatchMapping()) {
|
||||
throw new IllegalArgumentException("Not a @BatchMapping method: " + info);
|
||||
}
|
||||
@@ -515,7 +351,7 @@ public class AnnotatedControllerConfigurer implements ApplicationContextAware, I
|
||||
}
|
||||
|
||||
HandlerMethod handlerMethod = info.getHandlerMethod();
|
||||
BatchLoaderHandlerMethod invocable = new BatchLoaderHandlerMethod(handlerMethod, this.executor);
|
||||
BatchLoaderHandlerMethod invocable = new BatchLoaderHandlerMethod(handlerMethod, getExecutor());
|
||||
|
||||
MethodParameter returnType = handlerMethod.getReturnType();
|
||||
Class<?> clazz = returnType.getParameterType();
|
||||
@@ -567,56 +403,12 @@ public class AnnotatedControllerConfigurer implements ApplicationContextAware, I
|
||||
}
|
||||
|
||||
|
||||
private static class MappingInfo {
|
||||
|
||||
private final FieldCoordinates coordinates;
|
||||
|
||||
private final boolean batchMapping;
|
||||
|
||||
private final int maxBatchSize;
|
||||
|
||||
private final HandlerMethod handlerMethod;
|
||||
|
||||
public MappingInfo(
|
||||
String typeName, String field, boolean batchMapping, int maxBatchSize,
|
||||
HandlerMethod handlerMethod) {
|
||||
|
||||
this.coordinates = FieldCoordinates.coordinates(typeName, field);
|
||||
this.batchMapping = batchMapping;
|
||||
this.maxBatchSize = maxBatchSize;
|
||||
this.handlerMethod = handlerMethod;
|
||||
}
|
||||
|
||||
public FieldCoordinates getCoordinates() {
|
||||
return this.coordinates;
|
||||
}
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean isBatchMapping() {
|
||||
return this.batchMapping;
|
||||
}
|
||||
|
||||
public int getMaxBatchSize() {
|
||||
return this.maxBatchSize;
|
||||
}
|
||||
|
||||
public HandlerMethod getHandlerMethod() {
|
||||
return this.handlerMethod;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.coordinates + " -> " + this.handlerMethod;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* {@link DataFetcher} that wrap and invokes a {@link HandlerMethod}.
|
||||
*/
|
||||
static class SchemaMappingDataFetcher implements SelfDescribingDataFetcher<Object> {
|
||||
|
||||
private final MappingInfo info;
|
||||
private final DataFetcherMappingInfo mappingInfo;
|
||||
|
||||
private final HandlerMethodArgumentResolverComposite argumentResolvers;
|
||||
|
||||
@@ -631,11 +423,11 @@ public class AnnotatedControllerConfigurer implements ApplicationContextAware, I
|
||||
private final boolean subscription;
|
||||
|
||||
SchemaMappingDataFetcher(
|
||||
MappingInfo info, HandlerMethodArgumentResolverComposite argumentResolvers,
|
||||
DataFetcherMappingInfo info, HandlerMethodArgumentResolverComposite argumentResolvers,
|
||||
@Nullable ValidationHelper helper, AnnotatedControllerExceptionResolver exceptionResolver,
|
||||
@Nullable Executor executor) {
|
||||
|
||||
this.info = info;
|
||||
this.mappingInfo = info;
|
||||
this.argumentResolvers = argumentResolvers;
|
||||
|
||||
this.methodValidationHelper =
|
||||
@@ -648,24 +440,24 @@ public class AnnotatedControllerConfigurer implements ApplicationContextAware, I
|
||||
this.exceptionResolver = exceptionResolver;
|
||||
|
||||
this.executor = executor;
|
||||
this.subscription = this.info.getCoordinates().getTypeName().equalsIgnoreCase("Subscription");
|
||||
this.subscription = this.mappingInfo.getCoordinates().getTypeName().equalsIgnoreCase("Subscription");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return this.info.getHandlerMethod().getShortLogMessage();
|
||||
return this.mappingInfo.getHandlerMethod().getShortLogMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolvableType getReturnType() {
|
||||
return ResolvableType.forMethodReturnType(this.info.getHandlerMethod().getMethod());
|
||||
return ResolvableType.forMethodReturnType(this.mappingInfo.getHandlerMethod().getMethod());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link HandlerMethod} used to fetch data.
|
||||
*/
|
||||
public HandlerMethod getHandlerMethod() {
|
||||
return this.info.getHandlerMethod();
|
||||
return this.mappingInfo.getHandlerMethod();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -728,21 +520,21 @@ public class AnnotatedControllerConfigurer implements ApplicationContextAware, I
|
||||
|
||||
static class BatchMappingDataFetcher implements DataFetcher<Object>, SelfDescribingDataFetcher<Object> {
|
||||
|
||||
private final MappingInfo info;
|
||||
private final DataFetcherMappingInfo mappingInfo;
|
||||
|
||||
private final ResolvableType returnType;
|
||||
|
||||
private final String dataLoaderKey;
|
||||
|
||||
BatchMappingDataFetcher(MappingInfo info, ResolvableType valueType, String dataLoaderKey) {
|
||||
this.info = info;
|
||||
BatchMappingDataFetcher(DataFetcherMappingInfo info, ResolvableType valueType, String dataLoaderKey) {
|
||||
this.mappingInfo = info;
|
||||
this.returnType = ResolvableType.forClassWithGenerics(CompletableFuture.class, valueType);
|
||||
this.dataLoaderKey = dataLoaderKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "@BatchMapping " + this.info.getHandlerMethod().getShortLogMessage();
|
||||
return "@BatchMapping " + this.mappingInfo.getHandlerMethod().getShortLogMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.lang.reflect.Method;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
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.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.format.FormatterRegistrar;
|
||||
import org.springframework.format.support.DefaultFormattingConversionService;
|
||||
import org.springframework.format.support.FormattingConversionService;
|
||||
import org.springframework.graphql.data.method.HandlerMethod;
|
||||
import org.springframework.graphql.data.method.HandlerMethodArgumentResolverComposite;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Convenient base for classes that find annotated controller method with argument
|
||||
* values resolved from a {@link graphql.schema.DataFetchingEnvironment}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.3
|
||||
* @param <M> the type of mapping info prepared from a controller method
|
||||
*/
|
||||
public abstract class AnnotatedControllerDetectionSupport<M> implements ApplicationContextAware, InitializingBean {
|
||||
|
||||
protected final static boolean springSecurityPresent = ClassUtils.isPresent(
|
||||
"org.springframework.security.core.context.SecurityContext",
|
||||
AnnotatedControllerDetectionSupport.class.getClassLoader());
|
||||
|
||||
/**
|
||||
* 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.";
|
||||
|
||||
|
||||
protected final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
|
||||
private final FormattingConversionService conversionService = new DefaultFormattingConversionService();
|
||||
|
||||
private boolean fallBackOnDirectFieldAccess;
|
||||
|
||||
@Nullable
|
||||
private Executor executor;
|
||||
|
||||
@Nullable
|
||||
private HandlerMethodArgumentResolverComposite argumentResolvers;
|
||||
|
||||
@Nullable
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
|
||||
/**
|
||||
* Add a {@code FormatterRegistrar} to customize the {@link ConversionService}
|
||||
* that assists in binding GraphQL arguments onto
|
||||
* {@link org.springframework.graphql.data.method.annotation.Argument @Argument}
|
||||
* annotated method parameters.
|
||||
*/
|
||||
public void addFormatterRegistrar(FormatterRegistrar registrar) {
|
||||
registrar.registerFormatters(this.conversionService);
|
||||
}
|
||||
|
||||
protected FormattingConversionService getConversionService() {
|
||||
return this.conversionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether binding GraphQL arguments onto
|
||||
* {@link org.springframework.graphql.data.method.annotation.Argument @Argument}
|
||||
* should falls back to direct field access in case the target object does
|
||||
* not use accessor methods.
|
||||
* @since 1.2.0
|
||||
*/
|
||||
public void setFallBackOnDirectFieldAccess(boolean fallBackOnDirectFieldAccess) {
|
||||
this.fallBackOnDirectFieldAccess = fallBackOnDirectFieldAccess;
|
||||
}
|
||||
|
||||
protected boolean isFallBackOnDirectFieldAccess() {
|
||||
return this.fallBackOnDirectFieldAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure an {@link Executor} to use for asynchronous handling of
|
||||
* {@link Callable} return values from controller methods.
|
||||
* <p>By default, this is not set in which case controller methods with a
|
||||
* {@code Callable} return value cannot be registered.
|
||||
* @param executor the executor to use
|
||||
*/
|
||||
public void setExecutor(Executor executor) {
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Executor getExecutor() {
|
||||
return this.executor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured argument resolvers.
|
||||
*/
|
||||
protected HandlerMethodArgumentResolverComposite getArgumentResolvers() {
|
||||
Assert.notNull(this.argumentResolvers, "Not yet initialized, was afterPropertiesSet called?");
|
||||
return this.argumentResolvers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected ApplicationContext getApplicationContext() {
|
||||
return this.applicationContext;
|
||||
}
|
||||
|
||||
protected final ApplicationContext obtainApplicationContext() {
|
||||
Assert.state(this.applicationContext != null, "No ApplicationContext");
|
||||
return this.applicationContext;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
this.argumentResolvers = initArgumentResolvers();
|
||||
}
|
||||
|
||||
protected abstract HandlerMethodArgumentResolverComposite initArgumentResolvers();
|
||||
|
||||
|
||||
/**
|
||||
* Scan beans in the ApplicationContext, detect and prepare a map of handler methods.
|
||||
*/
|
||||
protected Set<M> detectHandlerMethods() {
|
||||
Set<M> results = new LinkedHashSet<>();
|
||||
ApplicationContext context = obtainApplicationContext();
|
||||
for (String beanName : context.getBeanNamesForType(Object.class)) {
|
||||
if (beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
|
||||
continue;
|
||||
}
|
||||
Class<?> beanType = null;
|
||||
try {
|
||||
beanType = context.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 || !AnnotatedElementUtils.hasAnnotation(beanType, Controller.class)) {
|
||||
continue;
|
||||
}
|
||||
Class<?> beanClass = context.getType(beanName);
|
||||
findHandlerMethods(beanName, beanClass).forEach(info -> {
|
||||
HandlerMethod handlerMethod = getHandlerMethod(info);
|
||||
M existing = results.stream().filter(o -> o.equals(info)).findFirst().orElse(null);
|
||||
if (existing != null && !getHandlerMethod(existing).equals(handlerMethod)) {
|
||||
throw new IllegalStateException(
|
||||
"Ambiguous mapping. Cannot map '" + handlerMethod.getBean() + "' method \n" +
|
||||
handlerMethod + "\n" + ": There is already '" +
|
||||
getHandlerMethod(existing).getBean() + "' bean method\n" + existing + " mapped.");
|
||||
}
|
||||
results.add(info);
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
protected abstract HandlerMethod getHandlerMethod(M mappingInfo);
|
||||
|
||||
private Collection<M> findHandlerMethods(Object handler, @Nullable Class<?> handlerClass) {
|
||||
if (handlerClass == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Class<?> userClass = ClassUtils.getUserClass(handlerClass);
|
||||
Map<Method, M> map = MethodIntrospector.selectMethods(
|
||||
userClass, (Method method) -> getMappingInfo(method, handler, userClass));
|
||||
|
||||
Collection<M> mappingInfos = map.values();
|
||||
|
||||
if (logger.isTraceEnabled() && !mappingInfos.isEmpty()) {
|
||||
logger.trace(formatMappings(userClass, mappingInfos));
|
||||
}
|
||||
|
||||
return mappingInfos;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected abstract M getMappingInfo(Method method, Object handler, Class<?> handlerType);
|
||||
|
||||
protected HandlerMethod createHandlerMethod(Method originalMethod, Object handler, Class<?> handlerType) {
|
||||
Method method = AopUtils.selectInvocableMethod(originalMethod, handlerType);
|
||||
return (handler instanceof String beanName ?
|
||||
new HandlerMethod(beanName, obtainApplicationContext().getAutowireCapableBeanFactory(), method) :
|
||||
new HandlerMethod(handler, method));
|
||||
}
|
||||
|
||||
private String formatMappings(Class<?> handlerType, Collection<M> infos) {
|
||||
String formattedType = Arrays.stream(ClassUtils.getPackageName(handlerType).split("\\."))
|
||||
.map(p -> p.substring(0, 1))
|
||||
.collect(Collectors.joining(".", "", "." + handlerType.getSimpleName()));
|
||||
return infos.stream()
|
||||
.map(info -> {
|
||||
Method method = getHandlerMethod(info).getMethod();
|
||||
String methodParameters = Arrays.stream(method.getGenericParameterTypes())
|
||||
.map(Type::getTypeName)
|
||||
.collect(Collectors.joining(",", "(", ")"));
|
||||
return info + methodParameters;
|
||||
})
|
||||
.collect(Collectors.joining("\n\t", "\n\t" + formattedType + ":" + "\n\t", ""));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -25,35 +25,21 @@ import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
import org.springframework.graphql.data.method.HandlerMethod;
|
||||
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
|
||||
import org.springframework.graphql.data.method.HandlerMethodArgumentResolverComposite;
|
||||
import org.springframework.graphql.data.method.InvocableHandlerMethodSupport;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* An extension of {@link HandlerMethod} for annotated handler methods adapted
|
||||
* to {@link graphql.schema.DataFetcher} with {@link DataFetchingEnvironment}
|
||||
* as their input.
|
||||
* Invocable handler to use as a {@link graphql.schema.DataFetcher}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class DataFetcherHandlerMethod extends InvocableHandlerMethodSupport {
|
||||
|
||||
private static final Object[] EMPTY_ARGS = new Object[0];
|
||||
|
||||
|
||||
private final HandlerMethodArgumentResolverComposite resolvers;
|
||||
public class DataFetcherHandlerMethod extends DataFetcherHandlerMethodSupport {
|
||||
|
||||
private final BiConsumer<Object, Object[]> validationHelper;
|
||||
|
||||
private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
|
||||
|
||||
private final boolean subscription;
|
||||
|
||||
@@ -70,22 +56,13 @@ public class DataFetcherHandlerMethod extends InvocableHandlerMethodSupport {
|
||||
@Nullable BiConsumer<Object, Object[]> validationHelper, @Nullable Executor executor,
|
||||
boolean subscription) {
|
||||
|
||||
super(handlerMethod, executor);
|
||||
super(handlerMethod, resolvers, executor);
|
||||
Assert.isTrue(!resolvers.getResolvers().isEmpty(), "No argument resolvers");
|
||||
this.resolvers = resolvers;
|
||||
this.validationHelper = (validationHelper != null ? validationHelper : (controller, args) -> {});
|
||||
this.subscription = subscription;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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 {@link DataFetchingEnvironment}.
|
||||
@@ -149,47 +126,6 @@ public class DataFetcherHandlerMethod extends InvocableHandlerMethodSupport {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}.
|
||||
*/
|
||||
private 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;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Object validateAndInvoke(Object[] args, DataFetchingEnvironment environment) {
|
||||
this.validationHelper.accept(getBean(), args);
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.concurrent.Executor;
|
||||
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
|
||||
import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
import org.springframework.graphql.data.method.HandlerMethod;
|
||||
import org.springframework.graphql.data.method.HandlerMethodArgumentResolverComposite;
|
||||
import org.springframework.graphql.data.method.InvocableHandlerMethodSupport;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
|
||||
/**
|
||||
* Extension of {@link InvocableHandlerMethodSupport} for handler methods that
|
||||
* resolve argument values from a {@link DataFetchingEnvironment}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.3
|
||||
*/
|
||||
public class DataFetcherHandlerMethodSupport extends InvocableHandlerMethodSupport {
|
||||
|
||||
private static final Object[] EMPTY_ARGS = new Object[0];
|
||||
|
||||
protected final HandlerMethodArgumentResolverComposite resolvers;
|
||||
|
||||
private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
|
||||
|
||||
|
||||
protected DataFetcherHandlerMethodSupport(
|
||||
HandlerMethod handlerMethod, HandlerMethodArgumentResolverComposite resolvers,
|
||||
@Nullable Executor executor) {
|
||||
|
||||
super(handlerMethod, executor);
|
||||
this.resolvers = resolvers;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the configured argument resolvers.
|
||||
*/
|
||||
public HandlerMethodArgumentResolverComposite getResolvers() {
|
||||
return this.resolvers;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the method argument values for the current request, checking the provided
|
||||
* argument values and falling back to the configured argument resolvers.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.FieldCoordinates;
|
||||
|
||||
import org.springframework.graphql.data.method.HandlerMethod;
|
||||
import org.springframework.graphql.data.method.annotation.BatchMapping;
|
||||
import org.springframework.graphql.data.method.annotation.SchemaMapping;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
|
||||
/**
|
||||
* Mapping information for a controller method to be registered as a
|
||||
* {@link graphql.schema.DataFetcher}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.3
|
||||
*/
|
||||
public final class DataFetcherMappingInfo {
|
||||
|
||||
private final FieldCoordinates coordinates;
|
||||
|
||||
private final boolean batchMapping;
|
||||
|
||||
private final int maxBatchSize;
|
||||
|
||||
private final HandlerMethod handlerMethod;
|
||||
|
||||
|
||||
public DataFetcherMappingInfo(
|
||||
String typeName, String field, boolean batchMapping, int maxBatchSize,
|
||||
HandlerMethod handlerMethod) {
|
||||
|
||||
this.coordinates = FieldCoordinates.coordinates(typeName, field);
|
||||
this.batchMapping = batchMapping;
|
||||
this.maxBatchSize = maxBatchSize;
|
||||
this.handlerMethod = handlerMethod;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The field to bind the controller method to.
|
||||
*/
|
||||
public FieldCoordinates getCoordinates() {
|
||||
return this.coordinates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether it is an {@link BatchMapping} method or not in which case it is
|
||||
* an {@link SchemaMapping} method.
|
||||
*/
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean isBatchMapping() {
|
||||
return this.batchMapping;
|
||||
}
|
||||
|
||||
/**
|
||||
* A batch size limit to apply for a batch mapping method, or -1 if a limit
|
||||
* does not apply.
|
||||
*/
|
||||
public int getMaxBatchSize() {
|
||||
return this.maxBatchSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* The controller method to use for data fetching.
|
||||
*/
|
||||
public HandlerMethod getHandlerMethod() {
|
||||
return this.handlerMethod;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getCoordinates().hashCode() * 31;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!(other instanceof DataFetcherMappingInfo otherInfo)) {
|
||||
return false;
|
||||
}
|
||||
return (this.coordinates.equals(otherInfo.coordinates));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.coordinates + " -> " + getHandlerMethod();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -530,7 +530,7 @@ class SchemaMappingInspectorTests {
|
||||
|
||||
@Test
|
||||
void reportUnmappedField() {
|
||||
String schema = """
|
||||
String schema = """
|
||||
type Query {
|
||||
allBooks: [Book]
|
||||
}
|
||||
@@ -550,13 +550,13 @@ class SchemaMappingInspectorTests {
|
||||
id: ID
|
||||
}
|
||||
""";
|
||||
SchemaReport report = inspectSchema(schema, BookController.class);
|
||||
SchemaReport report = inspectSchema(schema, BookController.class);
|
||||
assertThatReport(report).hasUnmappedFieldCount(1).hasSkippedTypeCount(0);
|
||||
assertThat(report.toString())
|
||||
.contains("GraphQL schema inspection:", "Unmapped fields: {Book=[missing]}", "Unmapped registrations:",
|
||||
"{Book.fetcher=BookController#fetcher[1 args]", " Query.paginatedBooks=BookController#paginatedBooks[0 args]",
|
||||
"Query.bookObject=BookController#bookObject[1 args]", "Query.bookById=BookController#bookById[1 args]",
|
||||
"Skipped types: []");
|
||||
assertThat(report.toString())
|
||||
.contains("GraphQL schema inspection:", "Unmapped fields: {Book=[missing]}", "Unmapped registrations:",
|
||||
"Book.fetcher=BookController#fetcher[1 args]", " Query.paginatedBooks=BookController#paginatedBooks[0 args]",
|
||||
"Query.bookObject=BookController#bookObject[1 args]", "Query.bookById=BookController#bookById[1 args]",
|
||||
"Skipped types: []");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user