Skip to content

Commit e88f2e5

Browse files
author
adriancole
committed
added IncrementalCallback type and updated Contract to process it
1 parent 8780a0c commit e88f2e5

8 files changed

Lines changed: 210 additions & 37 deletions

File tree

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,18 @@
1515
*/
1616
package feign;
1717

18+
import javax.inject.Named;
1819
import java.lang.annotation.Annotation;
1920
import java.lang.reflect.Method;
21+
import java.lang.reflect.Type;
2022
import java.net.URI;
2123
import java.util.ArrayList;
2224
import java.util.Collection;
2325
import java.util.List;
2426

25-
import javax.inject.Named;
26-
2727
import static feign.Util.checkState;
2828
import static feign.Util.emptyToNull;
29+
import static feign.Util.resolveLastTypeParameter;
2930

3031
/**
3132
* Defines what annotations and values are valid on interfaces.
@@ -50,7 +51,7 @@ public List<MethodMetadata> parseAndValidatateMetadata(Class<?> declaring) {
5051
*/
5152
public MethodMetadata parseAndValidatateMetadata(Method method) {
5253
MethodMetadata data = new MethodMetadata();
53-
data.returnType(method.getGenericReturnType());
54+
data.decodeInto(method.getGenericReturnType());
5455
data.configKey(Feign.configKey(method));
5556

5657
for (Annotation methodAnnotation : method.getAnnotations()) {
@@ -69,8 +70,17 @@ public MethodMetadata parseAndValidatateMetadata(Method method) {
6970
}
7071
if (parameterTypes[i] == URI.class) {
7172
data.urlIndex(i);
73+
} else if (IncrementalCallback.class.isAssignableFrom(parameterTypes[i])) {
74+
checkState(method.getReturnType() == void.class, "IncrementalCallback methods must return void: %s", method);
75+
checkState(i == count - 1, "IncrementalCallback must be the last parameter: %s", method);
76+
Type context = method.getGenericParameterTypes()[i];
77+
Type incrementalCallbackType = resolveLastTypeParameter(context, IncrementalCallback.class);
78+
data.decodeInto(incrementalCallbackType);
79+
data.incrementalCallbackIndex(i);
80+
checkState(incrementalCallbackType != null, "Expected param %s to be IncrementalCallback<X> or IncrementalCallback<? super X> or a subtype",
81+
context, incrementalCallbackType);
7282
} else if (!isHttpAnnotation) {
73-
checkState(data.formParams().isEmpty(), "Body parameters cannot be used with @FormParam parameters.");
83+
checkState(data.formParams().isEmpty(), "Body parameters cannot be used with form parameters.");
7484
checkState(data.bodyIndex() == null, "Method has too many Body parameters: %s", method);
7585
data.bodyIndex(i);
7686
data.bodyType(method.getGenericParameterTypes()[i]);
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package feign;
2+
3+
/**
4+
* Communicates results as they are {@link feign.codec.Decoder decoded} from
5+
* an {@link Response.Body http response body}. {@link #onNext(Object) onNext}
6+
* will be called for each incremental value of type {@code T}, or not at all
7+
* when there are no values present in the response. Methods that accept
8+
* {@code IncrementalCallback} are asynchronous, which implies background
9+
* processing.
10+
* <br>
11+
* {@link #onSuccess() onSuccess} or {@link #onFailure(Throwable)} onFailure}
12+
* will be called when the response is finished, but not both.
13+
* <br>
14+
* {@code IncrementalCallback} can be used as an asynchronous alternative to a
15+
* {@code Collection}, or any other use where iterative response parsing is
16+
* worth the additional effort to implement this interface.
17+
* <br>
18+
* <br>
19+
* Here's an example of implementing {@code IncrementalCallback}:
20+
* <br>
21+
* <pre>
22+
* IncrementalCallback<Contributor> counter = new IncrementalCallback<Contributor>() {
23+
*
24+
* public int count;
25+
*
26+
* &#064;Override public void onNext(Contributor element) {
27+
* count++;
28+
* }
29+
*
30+
* &#064;Override public void onSuccess() {
31+
* System.out.println("found " + count + " contributors");
32+
* }
33+
*
34+
* &#064;Override public void onFailure(Throwable cause) {
35+
* System.err.println("sad face after contributor " + count);
36+
* }
37+
* };
38+
* github.contributors("netflix", "feign", counter);
39+
* </pre>
40+
*
41+
* @param <T> expected value to decode
42+
*/
43+
public interface IncrementalCallback<T> {
44+
/**
45+
* Invoked as soon as new data is available. Could be invoked many times or
46+
* not at all.
47+
*
48+
* @param element next decoded element.
49+
*/
50+
void onNext(T element);
51+
52+
/**
53+
* Called when response processing completed successfully.
54+
*/
55+
void onSuccess();
56+
57+
/**
58+
* Called when response processing failed for any reason.
59+
* <br>
60+
* Common failure cases include {@link FeignException},
61+
* {@link java.io.IOException}, and {@link feign.codec.DecodeException}.
62+
* However, the cause could be a {@code Throwable} of any kind.
63+
*
64+
* @param cause the reason for the failure
65+
*/
66+
void onFailure(Throwable cause);
67+
}

feign-core/src/main/java/feign/MethodHandler.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,13 @@ private SynchronousMethodHandler(Target<?> target, Client client, Provider<Retry
7272
}
7373

7474
@Override protected Object decode(Object[] argv, Response response) throws Throwable {
75-
if (metadata.returnType().equals(Response.class)) {
75+
if (metadata.decodeInto().equals(Response.class)) {
7676
return response;
77-
} else if (metadata.returnType() == void.class || response.body() == null) {
77+
} else if (metadata.decodeInto() == void.class || response.body() == null) {
7878
return null;
7979
}
8080
try {
81-
return decoder.decode(response.body().asReader(), metadata.returnType());
81+
return decoder.decode(response.body().asReader(), metadata.decodeInto());
8282
} catch (FeignException e) {
8383
throw e;
8484
} catch (RuntimeException e) {

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

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@
2424
import java.util.Map;
2525

2626
public final class MethodMetadata implements Serializable {
27+
2728
MethodMetadata() {
2829
}
2930

3031
private String configKey;
31-
private transient Type returnType;
32+
private transient Type decodeInto;
3233
private Integer urlIndex;
34+
private Integer incrementalCallbackIndex;
3335
private Integer bodyIndex;
3436
private transient Type bodyType;
3537
private RequestTemplate template = new RequestTemplate();
@@ -48,12 +50,16 @@ MethodMetadata configKey(String configKey) {
4850
return this;
4951
}
5052

51-
public Type returnType() {
52-
return returnType;
53+
/**
54+
* Method return type unless there is an {@link IncrementalCallback} arg. In this case, it is the type parameter of the
55+
* incrementalCallback.
56+
*/
57+
public Type decodeInto() {
58+
return decodeInto;
5359
}
5460

55-
MethodMetadata returnType(Type returnType) {
56-
this.returnType = returnType;
61+
MethodMetadata decodeInto(Type decodeInto) {
62+
this.decodeInto = decodeInto;
5763
return this;
5864
}
5965

@@ -66,6 +72,15 @@ MethodMetadata urlIndex(Integer urlIndex) {
6672
return this;
6773
}
6874

75+
public Integer incrementalCallbackIndex() {
76+
return incrementalCallbackIndex;
77+
}
78+
79+
MethodMetadata incrementalCallbackIndex(Integer incrementalCallbackIndex) {
80+
this.incrementalCallbackIndex = incrementalCallbackIndex;
81+
return this;
82+
}
83+
6984
public Integer bodyIndex() {
7085
return bodyIndex;
7186
}
@@ -97,4 +112,5 @@ public Map<Integer, Collection<String>> indexToName() {
97112
}
98113

99114
private static final long serialVersionUID = 1L;
115+
100116
}

feign-core/src/main/java/feign/ReflectiveFeign.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,13 +161,13 @@ public Map<String, MethodHandler> apply(Target key) {
161161
List<MethodMetadata> metadata = contract.parseAndValidatateMetadata(key.type());
162162
Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>();
163163
for (MethodMetadata md : metadata) {
164-
Decoder.TextStream decoder = decoders.get(md.returnType());
164+
Decoder.TextStream decoder = decoders.get(md.decodeInto());
165165
if (decoder == null) {
166166
decoder = decoders.get(Object.class);
167167
}
168168
if (decoder == null) {
169169
throw new IllegalStateException(format("%s needs @Provides(type = Set) Decoder decoder()" +
170-
"{ // Decoder.TextStream<%s> or Decoder.TextStream<Object>}", md.configKey(), md.returnType()));
170+
"{ // Decoder.TextStream<%s> or Decoder.TextStream<Object>}", md.configKey(), md.decodeInto()));
171171
}
172172
BuildTemplateByResolvingArgs buildTemplate;
173173
if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) {
@@ -183,7 +183,7 @@ public Map<String, MethodHandler> apply(Target key) {
183183
}
184184
if (encoder == null) {
185185
throw new IllegalStateException(format("%s needs @Provides(type = Set) Encoder encoder()" +
186-
"{ // Encoder.Text<%s> or Encoder.Text<Object>}", md.bodyType(), md.returnType()));
186+
"{ // Encoder.Text<%s> or Encoder.Text<Object>}", md.bodyType(), md.decodeInto()));
187187
}
188188
buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder);
189189
} else {

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.testng.annotations.Test;
2222

2323
import javax.inject.Named;
24+
import java.lang.reflect.Type;
2425
import java.net.URI;
2526
import java.util.List;
2627

@@ -237,4 +238,45 @@ interface HeaderParams {
237238
assertEquals(md.template().headers().get("Auth-Token"), ImmutableSet.of("{Auth-Token}"));
238239
assertEquals(md.indexToName().get(0), ImmutableSet.of("Auth-Token"));
239240
}
241+
242+
interface WithIncrementalCallback {
243+
@RequestLine("GET /") void valid(IncrementalCallback<List<String>> one);
244+
245+
@RequestLine("GET /{path}") void badOrder(IncrementalCallback<List<String>> one, @Named("path") String path);
246+
247+
@RequestLine("GET /") Response returnType(IncrementalCallback<List<String>> one);
248+
249+
@RequestLine("GET /") void wildcardExtends(IncrementalCallback<? extends List<String>> one);
250+
251+
@RequestLine("GET /") void subtype(ParameterizedIncrementalCallback<List<String>> one);
252+
}
253+
254+
static final List<String> listString = null;
255+
256+
interface ParameterizedIncrementalCallback<T extends List<String>> extends IncrementalCallback<T> {
257+
}
258+
259+
@Test public void methodCanHaveIncrementalCallbackParam() throws Exception {
260+
contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("valid", IncrementalCallback.class));
261+
}
262+
263+
@Test public void methodMetadataReturnTypeOnObservableMethodIsItsTypeParameter() throws Exception {
264+
Type listStringType = getClass().getDeclaredField("listString").getGenericType();
265+
MethodMetadata md = contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("valid", IncrementalCallback.class));
266+
assertEquals(md.decodeInto(), listStringType);
267+
md = contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("wildcardExtends", IncrementalCallback.class));
268+
assertEquals(md.decodeInto(), listStringType);
269+
md = contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("subtype", ParameterizedIncrementalCallback.class));
270+
assertEquals(md.decodeInto(), listStringType);
271+
}
272+
273+
@Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = ".*the last parameter.*")
274+
public void incrementalCallbackParamMustBeLast() throws Exception {
275+
contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("badOrder", IncrementalCallback.class, String.class));
276+
}
277+
278+
@Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = ".*must return void.*")
279+
public void incrementalCallbackMethodMustReturnVoid() throws Exception {
280+
contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("returnType", IncrementalCallback.class));
281+
}
240282
}

feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@
1515
*/
1616
package feign.jaxrs;
1717

18-
import java.lang.annotation.Annotation;
19-
import java.lang.reflect.Method;
20-
import java.util.Collection;
18+
import dagger.Provides;
19+
import feign.Body;
20+
import feign.Contract;
21+
import feign.MethodMetadata;
2122

2223
import javax.ws.rs.Consumes;
2324
import javax.ws.rs.FormParam;
@@ -27,11 +28,9 @@
2728
import javax.ws.rs.PathParam;
2829
import javax.ws.rs.Produces;
2930
import javax.ws.rs.QueryParam;
30-
31-
import dagger.Provides;
32-
import feign.Body;
33-
import feign.Contract;
34-
import feign.MethodMetadata;
31+
import java.lang.annotation.Annotation;
32+
import java.lang.reflect.Method;
33+
import java.util.Collection;
3534

3635
import static feign.Util.checkState;
3736

@@ -44,7 +43,7 @@ public final class JAXRSModule {
4443
return new JAXRSContract();
4544
}
4645

47-
static final class JAXRSContract extends Contract {
46+
public static final class JAXRSContract extends Contract {
4847

4948
@Override
5049
protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) {

feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,13 @@
1717

1818
import com.google.common.collect.ImmutableList;
1919
import com.google.common.collect.ImmutableSet;
20-
2120
import com.google.gson.reflect.TypeToken;
22-
import feign.RequestLine;
21+
import feign.Body;
22+
import feign.IncrementalCallback;
23+
import feign.MethodMetadata;
24+
import feign.Response;
2325
import org.testng.annotations.Test;
2426

25-
import java.lang.annotation.ElementType;
26-
import java.lang.annotation.Retention;
27-
import java.lang.annotation.RetentionPolicy;
28-
import java.lang.annotation.Target;
29-
import java.net.URI;
30-
import java.util.List;
31-
3227
import javax.ws.rs.DELETE;
3328
import javax.ws.rs.FormParam;
3429
import javax.ws.rs.GET;
@@ -40,10 +35,13 @@
4035
import javax.ws.rs.PathParam;
4136
import javax.ws.rs.Produces;
4237
import javax.ws.rs.QueryParam;
43-
44-
import feign.Body;
45-
import feign.MethodMetadata;
46-
import feign.Response;
38+
import java.lang.annotation.ElementType;
39+
import java.lang.annotation.Retention;
40+
import java.lang.annotation.RetentionPolicy;
41+
import java.lang.annotation.Target;
42+
import java.lang.reflect.Type;
43+
import java.net.URI;
44+
import java.util.List;
4745

4846
import static feign.jaxrs.JAXRSModule.CONTENT_TYPE;
4947
import static javax.ws.rs.HttpMethod.DELETE;
@@ -264,4 +262,45 @@ interface HeaderParams {
264262
assertEquals(md.template().headers().get("Auth-Token"), ImmutableSet.of("{Auth-Token}"));
265263
assertEquals(md.indexToName().get(0), ImmutableSet.of("Auth-Token"));
266264
}
265+
266+
interface WithIncrementalCallback {
267+
@GET @Path("/") void valid(IncrementalCallback<List<String>> one);
268+
269+
@GET @Path("/{path}") void badOrder(IncrementalCallback<List<String>> one, @PathParam("path") String path);
270+
271+
@GET @Path("/") Response returnType(IncrementalCallback<List<String>> one);
272+
273+
@GET @Path("/") void wildcardExtends(IncrementalCallback<? extends List<String>> one);
274+
275+
@GET @Path("/") void subtype(ParameterizedIncrementalCallback<List<String>> one);
276+
}
277+
278+
static final List<String> listString = null;
279+
280+
interface ParameterizedIncrementalCallback<T extends List<String>> extends IncrementalCallback<T> {
281+
}
282+
283+
@Test public void methodCanHaveIncrementalCallbackParam() throws Exception {
284+
contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("valid", IncrementalCallback.class));
285+
}
286+
287+
@Test public void methodMetadataReturnTypeOnObservableMethodIsItsTypeParameter() throws Exception {
288+
Type listStringType = getClass().getDeclaredField("listString").getGenericType();
289+
MethodMetadata md = contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("valid", IncrementalCallback.class));
290+
assertEquals(md.decodeInto(), listStringType);
291+
md = contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("wildcardExtends", IncrementalCallback.class));
292+
assertEquals(md.decodeInto(), listStringType);
293+
md = contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("subtype", ParameterizedIncrementalCallback.class));
294+
assertEquals(md.decodeInto(), listStringType);
295+
}
296+
297+
@Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = ".*the last parameter.*")
298+
public void incrementalCallbackParamMustBeLast() throws Exception {
299+
contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("badOrder", IncrementalCallback.class, String.class));
300+
}
301+
302+
@Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = ".*must return void.*")
303+
public void incrementalCallbackMethodMustReturnVoid() throws Exception {
304+
contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("returnType", IncrementalCallback.class));
305+
}
267306
}

0 commit comments

Comments
 (0)