Skip to content

Commit 2a80358

Browse files
authored
Declarative contracts (#1060)
* Declarative contracts * Actually using the data structure to read declaritve contracts * Using declarative contract for jaxrs contracts * Make possible for contracts to declare parameters as ignored * Using predicate to decide if an AnnotationProcessor should be invoked * Restore environment variable for GITHUB_TOKEN
1 parent a407155 commit 2a80358

File tree

11 files changed

+445
-237
lines changed

11 files changed

+445
-237
lines changed

.travis.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,9 @@ jobs:
3232
jdk: openjdk8
3333
install: true
3434
script:
35-
- ./mvnw -B -nsu -s ./travis/settings.xml -P release -pl -:feign-benchmark -DskipTests=true deploy
35+
- ./mvnw -B -nsu -s ./travis/settings.xml -P release -pl -:feign-benchmark -DskipTests=true deploy
36+
37+
env:
38+
global:
39+
# Ex. travis encrypt GITHUB_TOKEN=token_for_tests
40+
- secure: "H4PuppuPE3lkvVQ1osulhgWeZmpIkDKj/z74lx4MUeDPNtcuqpwmTVWtL5Zyjf8CxlALX2djx4RIBshaQAu4GtKarPLONinNLZ/TCtoK8dF08/ESxLEiLQzwGkS+geWoEFiZncB5Px2T7ZbUfVFO3crVY9CLn35znR8k1uidocL0JlyVPGwCwuBxFmDhs3BZh3JvbwSikAVRvlCRU6BbREFQbSK1EamuUju/rlo+dx7W5tiiuEJJ50c8vpgatTFyy821YP82fMRrhuBDpS4/rsL9DmLhQTEbCjZW+22DhEFPRlo0XIfidC7APybXnu3oO+jFuGaFKiQdy7sjB03g/Bz5H7jAIAkbl8UpbjN+IoeUU/OgMuBYf5wJjPDYUEdI3CXqywPn0xYZwVsOcSg+UkQGYdW9ux/U+nKsYLXLWWhst2QMFzbmO94KCrpgCW4mshr/5WP4XU6cEJwDsKMAUPWuOk0KMMjIufSgvPvteWZwT9akZwzEMuGaUQ5kLr1X6xTPv1cKXTreitaoOLQs28kmPVfTwVEdareaSVXcRqeflJJBSXkAgBqGhV5CAEUaUgt9/QD0Jj5RGyRPllFcydXVLTPeg62X/L5COswlvJhPkvfNnkbMpDQZYojKKPmAf+UqZJmVYPpOoNEXygldueKeunWkna/wYkMj0YnOkM8="

core/src/main/java/feign/Contract.java

Lines changed: 241 additions & 79 deletions
Large diffs are not rendered by default.

core/src/main/java/feign/MethodMetadata.java

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,7 @@
1515

1616
import java.io.Serializable;
1717
import java.lang.reflect.Type;
18-
import java.util.ArrayList;
19-
import java.util.Collection;
20-
import java.util.LinkedHashMap;
21-
import java.util.List;
22-
import java.util.Map;
18+
import java.util.*;
2319
import feign.Param.Expander;
2420

2521
public final class MethodMetadata implements Serializable {
@@ -41,6 +37,7 @@ public final class MethodMetadata implements Serializable {
4137
new LinkedHashMap<Integer, Class<? extends Expander>>();
4238
private Map<Integer, Boolean> indexToEncoded = new LinkedHashMap<Integer, Boolean>();
4339
private transient Map<Integer, Expander> indexToExpander;
40+
private BitSet parameterToIgnore = new BitSet();
4441

4542
MethodMetadata() {}
4643

@@ -163,4 +160,50 @@ public MethodMetadata indexToExpander(Map<Integer, Expander> indexToExpander) {
163160
public Map<Integer, Expander> indexToExpander() {
164161
return indexToExpander;
165162
}
163+
164+
/**
165+
* @param i individual parameter that should be ignored
166+
* @return this instance
167+
*/
168+
public MethodMetadata ignoreParamater(int i) {
169+
this.parameterToIgnore.set(i);
170+
return this;
171+
}
172+
173+
public BitSet parameterToIgnore() {
174+
return parameterToIgnore;
175+
}
176+
177+
public MethodMetadata parameterToIgnore(BitSet parameterToIgnore) {
178+
this.parameterToIgnore = parameterToIgnore;
179+
return this;
180+
}
181+
182+
/**
183+
* @param i individual parameter to check if should be ignored
184+
* @return true when field should not be processed by feign
185+
*/
186+
public boolean shouldIgnoreParamater(int i) {
187+
return parameterToIgnore.get(i);
188+
}
189+
190+
/**
191+
* @param index
192+
* @return true if the parameter {@code index} was already consumed by a any
193+
* {@link MethodMetadata} holder
194+
*/
195+
public boolean isAlreadyProcessed(Integer index) {
196+
return index.equals(urlIndex)
197+
|| index.equals(bodyIndex)
198+
|| index.equals(headerMapIndex)
199+
|| index.equals(queryMapIndex)
200+
|| indexToName.containsKey(index)
201+
|| indexToExpanderClass.containsKey(index)
202+
|| indexToEncoded.containsKey(index)
203+
|| (indexToExpander != null && indexToExpander.containsKey(index))
204+
|| parameterToIgnore.get(index);
205+
}
206+
207+
208+
166209
}

core/src/main/java/feign/SynchronousMethodHandler.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,9 @@ Options findOptions(Object[] argv) {
187187
if (argv == null || argv.length == 0) {
188188
return this.options;
189189
}
190-
return (Options) Stream.of(argv)
191-
.filter(o -> o instanceof Options)
190+
return Stream.of(argv)
191+
.filter(Options.class::isInstance)
192+
.map(Options.class::cast)
192193
.findFirst()
193194
.orElse(this.options);
194195
}

core/src/test/java/feign/ContractWithRuntimeInjectionTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ Contract contract(BeanFactory beanFactory) {
8282
}
8383
}
8484

85-
static class ContractWithRuntimeInjection extends Contract.Default {
85+
static class ContractWithRuntimeInjection implements Contract {
8686
final BeanFactory beanFactory;
8787

8888
ContractWithRuntimeInjection(BeanFactory beanFactory) {
@@ -94,7 +94,7 @@ static class ContractWithRuntimeInjection extends Contract.Default {
9494
*/
9595
@Override
9696
public List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType) {
97-
List<MethodMetadata> result = super.parseAndValidatateMetadata(targetType);
97+
List<MethodMetadata> result = new Contract.Default().parseAndValidatateMetadata(targetType);
9898
for (MethodMetadata md : result) {
9999
Map<Integer, Param.Expander> indexToExpander = new LinkedHashMap<Integer, Param.Expander>();
100100
for (Map.Entry<Integer, Class<? extends Param.Expander>> entry : md.indexToExpanderClass()

core/src/test/java/feign/DefaultContractTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -814,7 +814,7 @@ interface MissingMethod {
814814
public void missingMethod() throws Exception {
815815
thrown.expect(IllegalStateException.class);
816816
thrown.expectMessage(
817-
"RequestLine annotation didn't start with an HTTP verb on method updateSharing");
817+
"RequestLine annotation didn't start with an HTTP verb on method MissingMethod#updateSharing");
818818

819819
contract.parseAndValidatateMetadata(MissingMethod.class);
820820
}

core/src/test/java/feign/assertj/RequestTemplateAssert.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,19 @@ public RequestTemplateAssert hasNoHeader(final String encoded) {
9393
objects.assertNull(info, actual.headers().get(encoded));
9494
return this;
9595
}
96+
97+
public RequestTemplateAssert noRequestBody() {
98+
isNotNull();
99+
if (actual.requestBody() != null) {
100+
if (actual.requestBody().bodyTemplate() != null) {
101+
failWithMessage("\nExpecting requestBody.bodyTemplate to be null, but was:<%s>",
102+
actual.requestBody().bodyTemplate());
103+
}
104+
if (actual.requestBody().asBytes() != null) {
105+
failWithMessage("\nExpecting requestBody.data to be null, but was:<%s>",
106+
actual.requestBody().asString());
107+
}
108+
}
109+
return this;
110+
}
96111
}

jaxrs/src/main/java/feign/jaxrs/JAXRSContract.java

Lines changed: 71 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,22 @@
1313
*/
1414
package feign.jaxrs;
1515

16-
import feign.Contract;
17-
import feign.MethodMetadata;
18-
import feign.Request;
19-
import javax.ws.rs.*;
20-
import java.lang.annotation.Annotation;
21-
import java.lang.reflect.Method;
22-
import java.util.ArrayList;
23-
import java.util.Arrays;
24-
import java.util.Collection;
25-
import java.util.Collections;
2616
import static feign.Util.checkState;
2717
import static feign.Util.emptyToNull;
2818
import static feign.Util.removeValues;
19+
import java.lang.annotation.Annotation;
20+
import java.lang.reflect.Method;
21+
import java.util.Collections;
22+
import javax.ws.rs.*;
23+
import feign.Contract.DeclarativeContract;
24+
import feign.MethodMetadata;
25+
import feign.Request;
2926

3027
/**
3128
* Please refer to the <a href="https://github.com/Netflix/feign/tree/master/feign-jaxrs">Feign
3229
* JAX-RS README</a>.
3330
*/
34-
public class JAXRSContract extends Contract.BaseContract {
31+
public class JAXRSContract extends DeclarativeContract {
3532

3633
static final String ACCEPT = "Accept";
3734
static final String CONTENT_TYPE = "Content-Type";
@@ -44,52 +41,49 @@ protected MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method me
4441
return super.parseAndValidateMetadata(targetType, method);
4542
}
4643

47-
@Override
48-
protected void processAnnotationOnClass(MethodMetadata data, Class<?> clz) {
49-
Path path = clz.getAnnotation(Path.class);
50-
if (path != null && !path.value().isEmpty()) {
51-
String pathValue = path.value();
52-
if (!pathValue.startsWith("/")) {
53-
pathValue = "/" + pathValue;
54-
}
55-
if (pathValue.endsWith("/")) {
56-
// Strip off any trailing slashes, since the template has already had slashes appropriately
57-
// added
58-
pathValue = pathValue.substring(0, pathValue.length() - 1);
44+
public JAXRSContract() {
45+
super.registerClassAnnotation(Path.class, (path, data) -> {
46+
if (path != null && !path.value().isEmpty()) {
47+
String pathValue = path.value();
48+
if (!pathValue.startsWith("/")) {
49+
pathValue = "/" + pathValue;
50+
}
51+
if (pathValue.endsWith("/")) {
52+
// Strip off any trailing slashes, since the template has already had slashes
53+
// appropriately
54+
// added
55+
pathValue = pathValue.substring(0, pathValue.length() - 1);
56+
}
57+
// jax-rs allows whitespace around the param name, as well as an optional regex. The
58+
// contract
59+
// should
60+
// strip these out appropriately.
61+
pathValue = pathValue.replaceAll("\\{\\s*(.+?)\\s*(:.+?)?\\}", "\\{$1\\}");
62+
data.template().uri(pathValue);
5963
}
60-
// jax-rs allows whitespace around the param name, as well as an optional regex. The contract
61-
// should
62-
// strip these out appropriately.
63-
pathValue = pathValue.replaceAll("\\{\\s*(.+?)\\s*(:.+?)?\\}", "\\{$1\\}");
64-
data.template().uri(pathValue);
65-
}
66-
Consumes consumes = clz.getAnnotation(Consumes.class);
67-
if (consumes != null) {
68-
handleConsumesAnnotation(data, consumes, clz.getName());
69-
}
70-
Produces produces = clz.getAnnotation(Produces.class);
71-
if (produces != null) {
72-
handleProducesAnnotation(data, produces, clz.getName());
73-
}
74-
}
64+
});
65+
super.registerClassAnnotation(Consumes.class, this::handleConsumesAnnotation);
66+
super.registerClassAnnotation(Produces.class, this::handleProducesAnnotation);
7567

76-
@Override
77-
protected void processAnnotationOnMethod(MethodMetadata data,
78-
Annotation methodAnnotation,
79-
Method method) {
80-
Class<? extends Annotation> annotationType = methodAnnotation.annotationType();
81-
HttpMethod http = annotationType.getAnnotation(HttpMethod.class);
82-
if (http != null) {
68+
registerMethodAnnotation(methodAnnotation -> {
69+
Class<? extends Annotation> annotationType = methodAnnotation.annotationType();
70+
HttpMethod http = annotationType.getAnnotation(HttpMethod.class);
71+
return http != null;
72+
}, (methodAnnotation, data) -> {
73+
Class<? extends Annotation> annotationType = methodAnnotation.annotationType();
74+
HttpMethod http = annotationType.getAnnotation(HttpMethod.class);
8375
checkState(data.template().method() == null,
84-
"Method %s contains multiple HTTP methods. Found: %s and %s", method.getName(),
76+
"Method %s contains multiple HTTP methods. Found: %s and %s", data.configKey(),
8577
data.template().method(), http.value());
8678
data.template().method(Request.HttpMethod.valueOf(http.value()));
87-
} else if (annotationType == Path.class) {
88-
String pathValue = emptyToNull(Path.class.cast(methodAnnotation).value());
79+
});
80+
81+
super.registerMethodAnnotation(Path.class, (path, data) -> {
82+
final String pathValue = emptyToNull(path.value());
8983
if (pathValue == null) {
9084
return;
9185
}
92-
String methodAnnotationValue = Path.class.cast(methodAnnotation).value();
86+
String methodAnnotationValue = path.value();
9387
if (!methodAnnotationValue.startsWith("/") && !data.template().url().endsWith("/")) {
9488
methodAnnotationValue = "/" + methodAnnotationValue;
9589
}
@@ -99,83 +93,62 @@ protected void processAnnotationOnMethod(MethodMetadata data,
9993
methodAnnotationValue =
10094
methodAnnotationValue.replaceAll("\\{\\s*(.+?)\\s*(:.+?)?\\}", "\\{$1\\}");
10195
data.template().uri(methodAnnotationValue, true);
102-
} else if (annotationType == Produces.class) {
103-
handleProducesAnnotation(data, (Produces) methodAnnotation, "method " + method.getName());
104-
} else if (annotationType == Consumes.class) {
105-
handleConsumesAnnotation(data, (Consumes) methodAnnotation, "method " + method.getName());
106-
}
96+
});
97+
super.registerMethodAnnotation(Consumes.class, this::handleConsumesAnnotation);
98+
super.registerMethodAnnotation(Produces.class, this::handleProducesAnnotation);
99+
100+
// trying to minimize the diff
101+
registerParamAnnotations();
107102
}
108103

109-
private void handleProducesAnnotation(MethodMetadata data, Produces produces, String name) {
110-
String[] serverProduces =
104+
private void handleProducesAnnotation(Produces produces, MethodMetadata data) {
105+
final String[] serverProduces =
111106
removeValues(produces.value(), (mediaType) -> emptyToNull(mediaType) == null, String.class);
112-
checkState(serverProduces.length > 0, "Produces.value() was empty on %s", name);
107+
checkState(serverProduces.length > 0, "Produces.value() was empty on %s", data.configKey());
113108
data.template().header(ACCEPT, Collections.emptyList()); // remove any previous produces
114109
data.template().header(ACCEPT, serverProduces);
115110
}
116111

117-
private void handleConsumesAnnotation(MethodMetadata data, Consumes consumes, String name) {
118-
String[] serverConsumes =
112+
private void handleConsumesAnnotation(Consumes consumes, MethodMetadata data) {
113+
final String[] serverConsumes =
119114
removeValues(consumes.value(), (mediaType) -> emptyToNull(mediaType) == null, String.class);
120-
checkState(serverConsumes.length > 0, "Consumes.value() was empty on %s", name);
115+
checkState(serverConsumes.length > 0, "Consumes.value() was empty on %s", data.configKey());
121116
data.template().header(CONTENT_TYPE, Collections.emptyList()); // remove any previous consumes
122117
data.template().header(CONTENT_TYPE, serverConsumes[0]);
123118
}
124119

125-
/**
126-
* Allows derived contracts to specify unsupported jax-rs parameter annotations which should be
127-
* ignored. Required for JAX-RS 2 compatibility.
128-
*/
129-
protected boolean isUnsupportedHttpParameterAnnotation(Annotation parameterAnnotation) {
130-
return false;
131-
}
132-
133-
@Override
134-
protected boolean processAnnotationsOnParameter(MethodMetadata data,
135-
Annotation[] annotations,
136-
int paramIndex) {
137-
boolean isHttpParam = false;
138-
for (Annotation parameterAnnotation : annotations) {
139-
Class<? extends Annotation> annotationType = parameterAnnotation.annotationType();
140-
// masc20180327. parameter with unsupported jax-rs annotations should not be passed as body
141-
// params.
142-
// this will prevent interfaces from becoming unusable entirely due to single (unsupported)
143-
// endpoints.
144-
// https://github.com/OpenFeign/feign/issues/669
145-
if (this.isUnsupportedHttpParameterAnnotation(parameterAnnotation)) {
146-
isHttpParam = true;
147-
} else if (annotationType == PathParam.class) {
148-
String name = PathParam.class.cast(parameterAnnotation).value();
120+
protected void registerParamAnnotations() {
121+
{
122+
registerParameterAnnotation(PathParam.class, (param, data, paramIndex) -> {
123+
final String name = param.value();
149124
checkState(emptyToNull(name) != null, "PathParam.value() was empty on parameter %s",
150125
paramIndex);
151126
nameParam(data, name, paramIndex);
152-
isHttpParam = true;
153-
} else if (annotationType == QueryParam.class) {
154-
String name = QueryParam.class.cast(parameterAnnotation).value();
127+
});
128+
registerParameterAnnotation(QueryParam.class, (param, data, paramIndex) -> {
129+
final String name = param.value();
155130
checkState(emptyToNull(name) != null, "QueryParam.value() was empty on parameter %s",
156131
paramIndex);
157-
String query = addTemplatedParam(name);
132+
final String query = addTemplatedParam(name);
158133
data.template().query(name, query);
159134
nameParam(data, name, paramIndex);
160-
isHttpParam = true;
161-
} else if (annotationType == HeaderParam.class) {
162-
String name = HeaderParam.class.cast(parameterAnnotation).value();
135+
});
136+
registerParameterAnnotation(HeaderParam.class, (param, data, paramIndex) -> {
137+
final String name = param.value();
163138
checkState(emptyToNull(name) != null, "HeaderParam.value() was empty on parameter %s",
164139
paramIndex);
165-
String header = addTemplatedParam(name);
140+
final String header = addTemplatedParam(name);
166141
data.template().header(name, header);
167142
nameParam(data, name, paramIndex);
168-
isHttpParam = true;
169-
} else if (annotationType == FormParam.class) {
170-
String name = FormParam.class.cast(parameterAnnotation).value();
143+
});
144+
registerParameterAnnotation(FormParam.class, (param, data, paramIndex) -> {
145+
final String name = param.value();
171146
checkState(emptyToNull(name) != null, "FormParam.value() was empty on parameter %s",
172147
paramIndex);
173148
data.formParams().add(name);
174149
nameParam(data, name, paramIndex);
175-
isHttpParam = true;
176-
}
150+
});
177151
}
178-
return isHttpParam;
179152
}
180153

181154
// Not using override as the super-type's method is deprecated and will be removed.

0 commit comments

Comments
 (0)