-
Notifications
You must be signed in to change notification settings - Fork 181
/
DefaultOperationBuilder.java
153 lines (131 loc) · 7.5 KB
/
DefaultOperationBuilder.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
package io.leangen.graphql.metadata.strategy.query;
import io.leangen.geantyref.GenericTypeReflector;
import io.leangen.graphql.annotations.GraphQLUnion;
import io.leangen.graphql.execution.GlobalEnvironment;
import io.leangen.graphql.generator.union.Union;
import io.leangen.graphql.metadata.Operation;
import io.leangen.graphql.metadata.OperationArgument;
import io.leangen.graphql.metadata.Resolver;
import io.leangen.graphql.metadata.exceptions.TypeMappingException;
import io.leangen.graphql.metadata.messages.MessageBundle;
import io.leangen.graphql.util.ClassUtils;
import io.leangen.graphql.util.Urls;
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @author Bojan Tomic (kaqqao)
*/
@SuppressWarnings("WeakerAccess")
public class DefaultOperationBuilder implements OperationBuilder {
private final TypeInference typeInference;
/**
* @param typeInference Controls automatic type inference if multiple resolver methods for the same operation return different types,
* or if different resolver methods specify arguments of the same name but of different types.
* <p>The inference process selects the most specific common super type of all the detected types.</p>
* <p>This feature is off by default as it can lead to surprising results when used unconsciously.</p>
*
* <p><b>Example:</b></p>
*
* <pre>
* {@code
* @GraphQLQuery(name = "numbers")
* public ArrayList<Long> getLongs(String paramOne) {...}
*
* @GraphQLQuery(name = "numbers")
* public LinkedList<Double> getDoubles(String paramTwo) {...}
* }
* </pre>
*
* The situation shown above would cause an exception without type inference enabled.
* With type inference, the return type would be treated as {@code AbstractList<Number>} as that
* is the most specific common super type of the two encountered types.
*/
public DefaultOperationBuilder(TypeInference typeInference) {
this.typeInference = typeInference;
}
@Override
public Operation buildQuery(Type contextType, List<Resolver> resolvers, GlobalEnvironment environment) {
String name = resolveName(resolvers);
AnnotatedType javaType = resolveJavaType(name, resolvers, environment.messageBundle);
List<OperationArgument> arguments = collectArguments(name, resolvers);
boolean batched = isBatched(resolvers);
return new Operation(name, javaType, contextType, arguments, resolvers, batched);
}
@Override
public Operation buildMutation(Type context, List<Resolver> resolvers, GlobalEnvironment environment) {
return buildQuery(context, resolvers, environment);
}
@Override
public Operation buildSubscription(Type context, List<Resolver> resolvers, GlobalEnvironment environment) {
return buildQuery(context, resolvers, environment);
}
protected String resolveName(List<Resolver> resolvers) {
return resolvers.get(0).getOperationName();
}
protected AnnotatedType resolveJavaType(String operationName, List<Resolver> resolvers, MessageBundle messageBundle) {
List<AnnotatedType> returnTypes = resolvers.stream()
.map(Resolver::getReturnType)
.collect(Collectors.toList());
if (resolvers.stream().anyMatch(resolver -> ClassUtils.containsTypeAnnotation(resolver.getReturnType(), GraphQLUnion.class))) {
return unionize(returnTypes.toArray(new AnnotatedType[returnTypes.size()]), messageBundle);
}
return resolveJavaType(returnTypes, "Multiple methods detected for operation \"" + operationName + "\" with different return types.");
}
//TODO do annotations or overloading decide what arg is required? should that decision be externalized?
protected List<OperationArgument> collectArguments(String operationName, List<Resolver> resolvers) {
Map<String, List<OperationArgument>> argumentsByName = resolvers.stream()
.flatMap(resolver -> resolver.getArguments().stream()) // merge all known args for this query
.collect(Collectors.groupingBy(OperationArgument::getName));
String errorPrefixTemplate = "Argument %s of operation \"" + operationName + "\" has different types in different resolver methods.";
return argumentsByName.keySet().stream()
.map(argName -> new OperationArgument(
resolveJavaType(argumentsByName.get(argName).stream().map(OperationArgument::getJavaType).collect(Collectors.toList()), String.format(errorPrefixTemplate, argName)),
argName,
argumentsByName.get(argName).stream().map(OperationArgument::getDescription).filter(Objects::nonNull).findFirst().orElse(""),
// argumentsByName.get(argName).size() == resolvers.size() || argumentsByName.get(argName).stream().anyMatch(OperationArgument::isRequired),
argumentsByName.get(argName).stream().map(OperationArgument::getDefaultValue).filter(Objects::nonNull).findFirst().orElse(null),
null,
argumentsByName.get(argName).stream().anyMatch(OperationArgument::isContext),
argumentsByName.get(argName).stream().anyMatch(OperationArgument::isMappable)
))
.collect(Collectors.toList());
}
protected boolean isBatched(List<Resolver> resolvers) {
return resolvers.stream().anyMatch(Resolver::isBatched);
}
protected AnnotatedType unionize(AnnotatedType[] types, MessageBundle messageBundle) {
return Union.unionize(types, messageBundle);
}
private AnnotatedType resolveJavaType(List<AnnotatedType> types, String errorPrefix) {
errorPrefix = errorPrefix + " Types found: " + Arrays.toString(types.stream().map(type -> type.getType().getTypeName()).toArray()) + ". ";
if (!typeInference.inferTypes && !types.stream().map(AnnotatedType::getType).allMatch(type -> type.equals(types.get(0).getType()))) {
throw new TypeMappingException(errorPrefix + "If this is intentional, and you wish GraphQL SPQR to infer the most " +
"common super type automatically, see " + Urls.Errors.CONFLICTING_RESOLVER_TYPES);
}
try {
return ClassUtils.getCommonSuperType(types, typeInference.allowObject ? GenericTypeReflector.annotate(Object.class) : null);
} catch (TypeMappingException e) {
throw new TypeMappingException(errorPrefix, e);
}
}
/**
* <p>{@code NONE} - No type inference. Results in a {@link TypeMappingException} if multiple different types are encountered.</p>
* <p>{@code LIMITED} - Automatically infer the common super type. Results in a {@link TypeMappingException} if no common ancestors except
* {@link Object}, {@link java.io.Serializable}, {@link Cloneable}, {@link Comparable} or {@link java.lang.annotation.Annotation} are found.</p>
* <p>{@code UNRESTRICTED} - Automatically infer the common super type. Results in {@link Object} if no common ancestors are found.</p>
*/
public enum TypeInference {
NONE(false, false), LIMITED(true, false), UNLIMITED(true, true);
public final boolean inferTypes;
public final boolean allowObject;
TypeInference(boolean inferTypes, boolean allowObject) {
this.inferTypes = inferTypes;
this.allowObject = allowObject;
}
}
}