Skip to content

Commit 1cacadb

Browse files
committed
allow multiple inheritance on the interface level with current restrictions intact.
1 parent 377ca74 commit 1cacadb

File tree

8 files changed

+355
-90
lines changed

8 files changed

+355
-90
lines changed

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

Lines changed: 108 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,14 @@
1313
*/
1414
package feign;
1515

16-
import static feign.Util.checkState;
17-
import static feign.Util.emptyToNull;
1816
import java.lang.annotation.Annotation;
1917
import java.lang.reflect.*;
2018
import java.net.URI;
2119
import java.util.*;
2220
import java.util.regex.Matcher;
2321
import java.util.regex.Pattern;
2422
import feign.Request.HttpMethod;
23+
import static feign.Util.*;
2524

2625
/**
2726
* Defines what annotations and values are valid on interfaces.
@@ -43,15 +42,7 @@ abstract class BaseContract implements Contract {
4342
*/
4443
@Override
4544
public List<MethodMetadata> parseAndValidateMetadata(Class<?> targetType) {
46-
checkState(targetType.getTypeParameters().length == 0, "Parameterized types unsupported: %s",
47-
targetType.getSimpleName());
48-
checkState(targetType.getInterfaces().length <= 1, "Only single inheritance supported: %s",
49-
targetType.getSimpleName());
50-
if (targetType.getInterfaces().length == 1) {
51-
checkState(targetType.getInterfaces()[0].getInterfaces().length == 0,
52-
"Only single-level inheritance supported: %s",
53-
targetType.getSimpleName());
54-
}
45+
validateTargetType(targetType);
5546
final Map<String, MethodMetadata> result = new LinkedHashMap<String, MethodMetadata>();
5647
for (final Method method : targetType.getMethods()) {
5748
if (method.getDeclaringClass() == Object.class ||
@@ -60,8 +51,20 @@ public List<MethodMetadata> parseAndValidateMetadata(Class<?> targetType) {
6051
continue;
6152
}
6253
final MethodMetadata metadata = parseAndValidateMetadata(targetType, method);
63-
checkState(!result.containsKey(metadata.configKey()), "Overrides unsupported: %s",
64-
metadata.configKey());
54+
if (result.containsKey(metadata.configKey())) {
55+
// there are two different scenarios here: getMethods() returns the overridden method
56+
// prior
57+
// to the inherited generic one or vice versa, e.g.
58+
// java.lang.Object get(...) may come before of after java.lang.String get(...)
59+
if (metadata.returnType().equals(Object.class)) {
60+
// ignore generic override
61+
continue;
62+
} else if (result.get(metadata.configKey()).returnType().equals(Object.class)) {
63+
// replace generic override
64+
result.put(metadata.configKey(), metadata);
65+
continue;
66+
}
67+
}
6568
result.put(metadata.configKey(), metadata);
6669
}
6770
return new ArrayList<>(result.values());
@@ -146,6 +149,84 @@ protected MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method me
146149
return data;
147150
}
148151

152+
private void validateTargetType(Class<?> targetType) {
153+
checkState(targetType.getTypeParameters().length == 0, "Parameterized types unsupported: %s",
154+
targetType.getSimpleName());
155+
checkState(targetType.getInterfaces().length <= 1, "Only single inheritance supported: %s",
156+
targetType.getSimpleName());
157+
// original contract: single level, single inheritance
158+
if (targetType.getInterfaces().length == 0
159+
|| targetType.getInterfaces()[0].getInterfaces().length == 0) {
160+
return;
161+
}
162+
163+
// recursively collect all inherited interfaces starting from superType and
164+
// validate these for offending annotations
165+
Class<?> superType = targetType.getInterfaces()[0];
166+
167+
Stack<Class<?>> candidateTypes = new Stack<>();
168+
List<Class<?>> inheritedTypes = new ArrayList<>();
169+
170+
candidateTypes.push(superType);
171+
while (!candidateTypes.empty()) {
172+
Class<?> inheritedType = candidateTypes.pop();
173+
for (Class<?> type : inheritedType.getInterfaces()) {
174+
candidateTypes.push(type);
175+
}
176+
if (superType.equals(inheritedType)) {
177+
continue;
178+
}
179+
inheritedTypes.add(inheritedType);
180+
}
181+
182+
if (!inheritedTypes.isEmpty()) {
183+
for (Class<?> inheritedType : inheritedTypes) {
184+
validateInheritedType(targetType, inheritedType);
185+
}
186+
}
187+
}
188+
189+
private void validateInheritedType(Class<?> targetType, Class<?> inheritedType) {
190+
for (Class<? extends Annotation> annotationClass : getNonInheritableTypeAnnotations()) {
191+
checkState(inheritedType.getAnnotationsByType(annotationClass).length == 0,
192+
"Multiple inheritance supported for plain types only: targetType: %s, inheritedType: %s, not inheritable: %s",
193+
targetType.getSimpleName(), inheritedType.getSimpleName(),
194+
annotationClass.getSimpleName());
195+
validateInheritedMethods(targetType, inheritedType);
196+
}
197+
}
198+
199+
private void validateInheritedMethods(Class<?> targetType, Class<?> inheritedType) {
200+
for (Method method : inheritedType.getDeclaredMethods()) {
201+
for (Class<? extends Annotation> annotationClass : getNonInheritableMethodAnnotations()) {
202+
checkState(method.getAnnotationsByType(annotationClass).length == 0,
203+
"Multiple inheritance supported for plain types only: targetType: %s, inheritedType: %s, method: %s, not inheritable: %s",
204+
targetType.getSimpleName(), inheritedType.getSimpleName(), method.getName(),
205+
annotationClass.getSimpleName());
206+
validateInheritedMethodParameters(targetType, inheritedType, method);
207+
}
208+
}
209+
}
210+
211+
private void validateInheritedMethodParameters(Class<?> targetType,
212+
Class<?> inheritedType,
213+
Method method) {
214+
for (Parameter parameter : method.getParameters()) {
215+
for (Class<? extends Annotation> annotationClass : getNonInheritableParameterAnnotations()) {
216+
checkState(parameter.getAnnotationsByType(annotationClass).length == 0,
217+
"Multiple inheritance supported for plain types only: targetType: %s, inheritedType: %s, method: %s, parameter: %s, not inheritable: %s",
218+
targetType.getSimpleName(), inheritedType.getSimpleName(), method.getName(),
219+
parameter.getName(), annotationClass.getSimpleName());
220+
}
221+
}
222+
}
223+
224+
protected abstract List<Class<? extends Annotation>> getNonInheritableTypeAnnotations();
225+
226+
protected abstract List<Class<? extends Annotation>> getNonInheritableMethodAnnotations();
227+
228+
protected abstract List<Class<? extends Annotation>> getNonInheritableParameterAnnotations();
229+
149230
private static void checkMapString(String name, Class<?> type, Type genericType) {
150231
checkState(Map.class.isAssignableFrom(type),
151232
"%s parameter must be a Map: %s", name, type);
@@ -310,5 +391,19 @@ private static Map<String, Collection<String>> toMap(String[] input) {
310391
return result;
311392
}
312393

394+
@Override
395+
protected List<Class<? extends Annotation>> getNonInheritableTypeAnnotations() {
396+
return Collections.singletonList(Headers.class);
397+
}
398+
399+
@Override
400+
protected List<Class<? extends Annotation>> getNonInheritableMethodAnnotations() {
401+
return Arrays.asList(Headers.class, RequestLine.class, Body.class);
402+
}
403+
404+
@Override
405+
protected List<Class<? extends Annotation>> getNonInheritableParameterAnnotations() {
406+
return Arrays.asList(HeaderMap.class, Param.class, QueryMap.class);
407+
}
313408
}
314409
}

core/src/main/java/feign/DeclarativeContract.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@
1313
*/
1414
package feign;
1515

16+
import feign.Contract.BaseContract;
1617
import java.lang.annotation.Annotation;
1718
import java.lang.reflect.Method;
1819
import java.lang.reflect.Parameter;
1920
import java.util.*;
2021
import java.util.function.Predicate;
2122
import java.util.stream.Collectors;
22-
import feign.Contract.BaseContract;
2323

2424
/**
2525
* {@link Contract} base implementation that works by declaring witch annotations should be

core/src/main/java/feign/Util.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@ public static void checkArgument(boolean expression,
104104
}
105105
}
106106

107+
public static void checkFail(String errorMessageTemplate,
108+
Object... errorMessageArgs) {
109+
throw new IllegalStateException(
110+
format(errorMessageTemplate, errorMessageArgs));
111+
}
112+
107113
/**
108114
* Copy of {@code com.google.common.base.Preconditions#checkNotNull}.
109115
*/
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/**
2+
* Copyright 2012-2020 The Feign Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5+
* in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License
10+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11+
* or implied. See the License for the specific language governing permissions and limitations under
12+
* the License.
13+
*/
14+
package feign;
15+
16+
import org.junit.Rule;
17+
import org.junit.Test;
18+
import org.junit.rules.ExpectedException;
19+
import java.util.List;
20+
import static feign.assertj.FeignAssertions.assertThat;
21+
import static java.util.Arrays.asList;
22+
import static org.assertj.core.data.MapEntry.entry;
23+
24+
/**
25+
* Tests interface inheritance defined per {@link Contract.Default} are interpreted into expected
26+
* {@link feign .RequestTemplate template} instances.
27+
*/
28+
public class DefaultContractInheritanceTest {
29+
30+
@Rule
31+
public final ExpectedException thrown = ExpectedException.none();
32+
33+
Contract.Default contract = new Contract.Default();
34+
35+
@Headers("Foo: Bar")
36+
interface SimpleParameterizedBaseApi<M> {
37+
38+
@RequestLine("GET /api/{zoneId}")
39+
M get(@Param("key") String key);
40+
}
41+
42+
interface SimpleParameterizedApi extends SimpleParameterizedBaseApi<String> {
43+
44+
}
45+
46+
@Test
47+
public void simpleParameterizedBaseApi() throws Exception {
48+
final List<MethodMetadata> md = contract.parseAndValidateMetadata(SimpleParameterizedApi.class);
49+
50+
assertThat(md).hasSize(1);
51+
52+
assertThat(md.get(0).configKey())
53+
.isEqualTo("SimpleParameterizedApi#get(String)");
54+
assertThat(md.get(0).returnType())
55+
.isEqualTo(String.class);
56+
assertThat(md.get(0).template())
57+
.hasHeaders(entry("Foo", asList("Bar")));
58+
}
59+
60+
@Test
61+
public void parameterizedApiUnsupported() throws Exception {
62+
thrown.expect(IllegalStateException.class);
63+
thrown.expectMessage("Parameterized types unsupported: SimpleParameterizedBaseApi");
64+
contract.parseAndValidateMetadata(SimpleParameterizedBaseApi.class);
65+
}
66+
67+
interface OverrideParameterizedApi extends SimpleParameterizedBaseApi<String> {
68+
69+
@Override
70+
@RequestLine("GET /api/{zoneId}")
71+
String get(@Param("key") String key);
72+
}
73+
74+
@Headers("Foo: Bar")
75+
interface SimpleBaseApi {
76+
77+
@RequestLine("GET /api/{zoneId}")
78+
String get(@Param("key") String key);
79+
}
80+
81+
interface OverrideSimpleApi extends SimpleBaseApi {
82+
83+
@RequestLine("GET /api/v2/{zoneId}")
84+
String get(@Param("key") String key);
85+
}
86+
87+
@Test
88+
public void overrideParameterizedApiSupported() {
89+
contract.parseAndValidateMetadata(OverrideParameterizedApi.class);
90+
}
91+
92+
@Test
93+
public void overrideSimpleApiSupported() {
94+
contract.parseAndValidateMetadata(OverrideSimpleApi.class);
95+
}
96+
97+
interface Child<T> extends SimpleParameterizedBaseApi<List<T>> {
98+
99+
}
100+
101+
interface GrandChild extends Child<String> {
102+
103+
}
104+
105+
interface SingleInheritanceChild extends SimpleParameterizedBaseApi<String> {
106+
}
107+
108+
interface FirstServiceBaseApi<T> {
109+
T get(String key);
110+
}
111+
112+
interface SecondServiceBaseApi<T> {
113+
void update(String key, T value);
114+
}
115+
116+
interface MultipleInheritanceDoneWrong
117+
extends SimpleParameterizedBaseApi<String>, FirstServiceBaseApi<String>,
118+
SecondServiceBaseApi<String> {
119+
}
120+
121+
@Headers("Foo: Bar")
122+
interface MultipleInheritanceParameterizedBaseApi<T>
123+
extends FirstServiceBaseApi<T>, SecondServiceBaseApi<T> {
124+
125+
@Override
126+
@RequestLine("GET /api/{zoneId}")
127+
T get(@Param("key") String key);
128+
129+
@Override
130+
@RequestLine("POST /api/{zoneId}")
131+
@Body("%7B\"value\": \"{value}\"%7D")
132+
void update(@Param("key") String key, @Param("value") T value);
133+
}
134+
135+
interface MultipleInheritanceDoneCorrectly
136+
extends MultipleInheritanceParameterizedBaseApi<String> {
137+
}
138+
139+
interface ServiceApi<T> extends FirstServiceBaseApi<T>, SecondServiceBaseApi<T> {
140+
}
141+
142+
@Headers("Foo: Bar")
143+
interface MultipleInheritanceApi extends ServiceApi<String> {
144+
145+
@Override
146+
@RequestLine("GET /api/{zoneId}")
147+
String get(@Param("key") String key);
148+
149+
@Override
150+
@RequestLine("POST /api/{zoneId}")
151+
@Body("%7B\"value\": \"{value}\"%7D")
152+
void update(@Param("key") String key, @Param("value") String value);
153+
}
154+
155+
@Test
156+
public void singleInheritance() {
157+
contract.parseAndValidateMetadata(SingleInheritanceChild.class);
158+
}
159+
160+
@Test
161+
public void multipleInheritanceDoneWrong() {
162+
thrown.expect(IllegalStateException.class);
163+
thrown.expectMessage("Only single inheritance supported: MultipleInheritanceDoneWrong");
164+
contract.parseAndValidateMetadata(MultipleInheritanceDoneWrong.class);
165+
}
166+
167+
@Test
168+
public void multipleInheritanceDoneCorrectly() {
169+
contract.parseAndValidateMetadata(MultipleInheritanceDoneCorrectly.class);
170+
}
171+
172+
@Test
173+
public void multipleInheritanceDoneCorrectly2() {
174+
contract.parseAndValidateMetadata(MultipleInheritanceApi.class);
175+
}
176+
177+
@Test
178+
public void multipleInheritanceForPlainTypesOnly() throws Exception {
179+
thrown.expect(IllegalStateException.class);
180+
thrown.expectMessage(
181+
"Multiple inheritance supported for plain types only: targetType: GrandChild, inheritedType: SimpleParameterizedBaseApi, not inheritable: Headers");
182+
contract.parseAndValidateMetadata(GrandChild.class);
183+
}
184+
}

0 commit comments

Comments
 (0)