forked from OpenFeign/feign
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathContract.java
More file actions
334 lines (305 loc) · 13.8 KB
/
Contract.java
File metadata and controls
334 lines (305 loc) · 13.8 KB
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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
/**
* Copyright 2012-2021 The Feign 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
*
* http://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 feign;
import feign.Request.HttpMethod;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static feign.Util.checkState;
import static feign.Util.emptyToNull;
/**
* Defines what annotations and values are valid on interfaces.
*/
public interface Contract {
/**
* Called to parse the methods in the class that are linked to HTTP requests.
*
* @param targetType {@link feign.Target#type() type} of the Feign interface.
*/
List<MethodMetadata> parseAndValidateMetadata(Class<?> targetType);
abstract class BaseContract implements Contract {
/**
* @param targetType {@link feign.Target#type() type} of the Feign interface.
* @see #parseAndValidateMetadata(Class)
*/
@Override
public List<MethodMetadata> parseAndValidateMetadata(Class<?> targetType) {
checkState(targetType.getTypeParameters().length == 0, "Parameterized types unsupported: %s",
targetType.getSimpleName());
checkState(targetType.getInterfaces().length <= 1, "Only single inheritance supported: %s",
targetType.getSimpleName());
final Map<String, MethodMetadata> result = new LinkedHashMap<String, MethodMetadata>();
for (final Method method : targetType.getMethods()) {
if (method.getDeclaringClass() == Object.class ||
(method.getModifiers() & Modifier.STATIC) != 0 ||
Util.isDefault(method)) {
continue;
}
final MethodMetadata metadata = parseAndValidateMetadata(targetType, method);
if (result.containsKey(metadata.configKey())) {
MethodMetadata existingMetadata = result.get(metadata.configKey());
Type existingReturnType = existingMetadata.returnType();
Type overridingReturnType = metadata.returnType();
Type resolvedType = Types.resolveReturnType(existingReturnType, overridingReturnType);
if (resolvedType.equals(overridingReturnType)) {
result.put(metadata.configKey(), metadata);
}
continue;
}
result.put(metadata.configKey(), metadata);
}
return new ArrayList<>(result.values());
}
/**
* @deprecated use {@link #parseAndValidateMetadata(Class, Method)} instead.
*/
@Deprecated
public MethodMetadata parseAndValidateMetadata(Method method) {
return parseAndValidateMetadata(method.getDeclaringClass(), method);
}
/**
* Called indirectly by {@link #parseAndValidateMetadata(Class)}.
*/
protected MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
final MethodMetadata data = new MethodMetadata();
data.targetType(targetType);
data.method(method);
data.returnType(
Types.resolve(targetType, targetType, method.getGenericReturnType()));
data.configKey(Feign.configKey(targetType, method));
if (AlwaysEncodeBodyContract.class.isAssignableFrom(this.getClass())) {
data.alwaysEncodeBody(true);
}
if (targetType.getInterfaces().length == 1) {
processAnnotationOnClass(data, targetType.getInterfaces()[0]);
}
processAnnotationOnClass(data, targetType);
for (final Annotation methodAnnotation : method.getAnnotations()) {
processAnnotationOnMethod(data, methodAnnotation, method);
}
if (data.isIgnored()) {
return data;
}
checkState(data.template().method() != null,
"Method %s not annotated with HTTP method type (ex. GET, POST)%s",
data.configKey(), data.warnings());
final Class<?>[] parameterTypes = method.getParameterTypes();
final Type[] genericParameterTypes = method.getGenericParameterTypes();
final Annotation[][] parameterAnnotations = method.getParameterAnnotations();
final int count = parameterAnnotations.length;
for (int i = 0; i < count; i++) {
boolean isHttpAnnotation = false;
if (parameterAnnotations[i] != null) {
isHttpAnnotation = processAnnotationsOnParameter(data, parameterAnnotations[i], i);
}
if (isHttpAnnotation) {
data.ignoreParamater(i);
}
if (parameterTypes[i] == URI.class) {
data.urlIndex(i);
} else if (!isHttpAnnotation && parameterTypes[i] != Request.Options.class) {
if (data.isAlreadyProcessed(i)) {
checkState(data.formParams().isEmpty() || data.bodyIndex() == null,
"Body parameters cannot be used with form parameters.%s", data.warnings());
} else if (!data.alwaysEncodeBody()) {
checkState(data.formParams().isEmpty(),
"Body parameters cannot be used with form parameters.%s", data.warnings());
checkState(data.bodyIndex() == null,
"Method has too many Body parameters: %s%s", method, data.warnings());
data.bodyIndex(i);
data.bodyType(
Types.resolve(targetType, targetType, genericParameterTypes[i]));
}
}
}
if (data.headerMapIndex() != null) {
checkMapString("HeaderMap", parameterTypes[data.headerMapIndex()],
genericParameterTypes[data.headerMapIndex()]);
}
if (data.queryMapIndex() != null) {
if (Map.class.isAssignableFrom(parameterTypes[data.queryMapIndex()])) {
checkMapKeys("QueryMap", genericParameterTypes[data.queryMapIndex()]);
}
}
return data;
}
private static void checkMapString(String name, Class<?> type, Type genericType) {
checkState(Map.class.isAssignableFrom(type),
"%s parameter must be a Map: %s", name, type);
checkMapKeys(name, genericType);
}
private static void checkMapKeys(String name, Type genericType) {
Class<?> keyClass = null;
// assume our type parameterized
if (ParameterizedType.class.isAssignableFrom(genericType.getClass())) {
final Type[] parameterTypes = ((ParameterizedType) genericType).getActualTypeArguments();
keyClass = (Class<?>) parameterTypes[0];
} else if (genericType instanceof Class<?>) {
// raw class, type parameters cannot be inferred directly, but we can scan any extended
// interfaces looking for any explict types
final Type[] interfaces = ((Class<?>) genericType).getGenericInterfaces();
for (final Type extended : interfaces) {
if (ParameterizedType.class.isAssignableFrom(extended.getClass())) {
// use the first extended interface we find.
final Type[] parameterTypes = ((ParameterizedType) extended).getActualTypeArguments();
keyClass = (Class<?>) parameterTypes[0];
break;
}
}
}
if (keyClass != null) {
checkState(String.class.equals(keyClass),
"%s key must be a String: %s", name, keyClass.getSimpleName());
}
}
/**
* Called by parseAndValidateMetadata twice, first on the declaring class, then on the target
* type (unless they are the same).
*
* @param data metadata collected so far relating to the current java method.
* @param clz the class to process
*/
protected abstract void processAnnotationOnClass(MethodMetadata data, Class<?> clz);
/**
* @param data metadata collected so far relating to the current java method.
* @param annotation annotations present on the current method annotation.
* @param method method currently being processed.
*/
protected abstract void processAnnotationOnMethod(MethodMetadata data,
Annotation annotation,
Method method);
/**
* @param data metadata collected so far relating to the current java method.
* @param annotations annotations present on the current parameter annotation.
* @param paramIndex if you find a name in {@code annotations}, call
* {@link #nameParam(MethodMetadata, String, int)} with this as the last parameter.
* @return true if you called {@link #nameParam(MethodMetadata, String, int)} after finding an
* http-relevant annotation.
*/
protected abstract boolean processAnnotationsOnParameter(MethodMetadata data,
Annotation[] annotations,
int paramIndex);
/**
* links a parameter name to its index in the method signature.
*/
protected void nameParam(MethodMetadata data, String name, int i) {
final Collection<String> names =
data.indexToName().containsKey(i) ? data.indexToName().get(i) : new ArrayList<String>();
names.add(name);
data.indexToName().put(i, names);
}
}
class Default extends DeclarativeContract {
static final Pattern REQUEST_LINE_PATTERN = Pattern.compile("^([A-Z]+)[ ]*(.*)$");
public Default() {
super.registerClassAnnotation(Headers.class, (header, data) -> {
final String[] headersOnType = header.value();
checkState(headersOnType.length > 0, "Headers annotation was empty on type %s.",
data.configKey());
final Map<String, Collection<String>> headers = toMap(headersOnType);
headers.putAll(data.template().headers());
data.template().headers(null); // to clear
data.template().headers(headers);
});
super.registerMethodAnnotation(RequestLine.class, (ann, data) -> {
final String requestLine = ann.value();
checkState(emptyToNull(requestLine) != null,
"RequestLine annotation was empty on method %s.", data.configKey());
final Matcher requestLineMatcher = REQUEST_LINE_PATTERN.matcher(requestLine);
if (!requestLineMatcher.find()) {
throw new IllegalStateException(String.format(
"RequestLine annotation didn't start with an HTTP verb on method %s",
data.configKey()));
} else {
data.template().method(HttpMethod.valueOf(requestLineMatcher.group(1)));
data.template().uri(requestLineMatcher.group(2));
}
data.template().decodeSlash(ann.decodeSlash());
data.template()
.collectionFormat(ann.collectionFormat());
});
super.registerMethodAnnotation(Body.class, (ann, data) -> {
final String body = ann.value();
checkState(emptyToNull(body) != null, "Body annotation was empty on method %s.",
data.configKey());
if (body.indexOf('{') == -1) {
data.template().body(body);
} else {
data.template().bodyTemplate(body);
}
});
super.registerMethodAnnotation(Headers.class, (header, data) -> {
final String[] headersOnMethod = header.value();
checkState(headersOnMethod.length > 0, "Headers annotation was empty on method %s.",
data.configKey());
data.template().headers(toMap(headersOnMethod));
});
super.registerParameterAnnotation(Param.class, (paramAnnotation, data, paramIndex) -> {
final String annotationName = paramAnnotation.value();
final Parameter parameter = data.method().getParameters()[paramIndex];
final String name;
if (emptyToNull(annotationName) == null && parameter.isNamePresent()) {
name = parameter.getName();
} else {
name = annotationName;
}
checkState(emptyToNull(name) != null, "Param annotation was empty on param %s.",
paramIndex);
nameParam(data, name, paramIndex);
final Class<? extends Param.Expander> expander = paramAnnotation.expander();
if (expander != Param.ToStringExpander.class) {
data.indexToExpanderClass().put(paramIndex, expander);
}
if (!data.template().hasRequestVariable(name)) {
data.formParams().add(name);
}
});
super.registerParameterAnnotation(QueryMap.class, (queryMap, data, paramIndex) -> {
checkState(data.queryMapIndex() == null,
"QueryMap annotation was present on multiple parameters.");
data.queryMapIndex(paramIndex);
data.queryMapEncoded(queryMap.encoded());
});
super.registerParameterAnnotation(HeaderMap.class, (queryMap, data, paramIndex) -> {
checkState(data.headerMapIndex() == null,
"HeaderMap annotation was present on multiple parameters.");
data.headerMapIndex(paramIndex);
});
}
private static Map<String, Collection<String>> toMap(String[] input) {
final Map<String, Collection<String>> result =
new LinkedHashMap<String, Collection<String>>(input.length);
for (final String header : input) {
final int colon = header.indexOf(':');
final String name = header.substring(0, colon);
if (!result.containsKey(name)) {
result.put(name, new ArrayList<String>(1));
}
result.get(name).add(header.substring(colon + 1).trim());
}
return result;
}
}
}