Browse Source

Removes Dagger 1.x Dependency and support for javax.inject.Named

Dagger 1.x and 2.x are incompatible. Rather than choose one over the
other, this change removes Dagger completely. Users can now choose any
injector, constructing Feign via its Builder.

This change also drops support for javax.inject.Named, which has
been replaced by feign.Param.

see #120
pull/141/head
Adrian Cole 10 years ago
parent
commit
8a0cba5cac
  1. 4
      CHANGELOG.md
  2. 104
      README.md
  3. 1
      build.gradle
  4. 3
      core/build.gradle
  5. 17
      core/src/main/java/feign/Client.java
  6. 10
      core/src/main/java/feign/Contract.java
  7. 171
      core/src/main/java/feign/Feign.java
  8. 1
      core/src/main/java/feign/InvocationHandlerFactory.java
  9. 23
      core/src/main/java/feign/ReflectiveFeign.java
  10. 14
      core/src/main/java/feign/RequestInterceptor.java
  11. 49
      core/src/main/java/feign/SynchronousMethodHandler.java
  12. 67
      core/src/test/java/feign/DefaultContractTest.java
  13. 233
      core/src/test/java/feign/FeignTest.java
  14. 2
      core/src/test/java/feign/LoggerTest.java
  15. 28
      core/src/test/java/feign/client/DefaultClientTest.java
  16. 6
      core/src/test/java/feign/client/TrustingSSLSocketFactory.java
  17. 2
      core/src/test/java/feign/examples/GitHubExample.java
  18. 178
      dagger.gradle
  19. 6
      gson/README.md
  20. 37
      gson/src/main/java/feign/gson/GsonCodec.java
  21. 9
      gson/src/main/java/feign/gson/GsonDecoder.java
  22. 8
      gson/src/main/java/feign/gson/GsonEncoder.java
  23. 46
      gson/src/main/java/feign/gson/GsonFactory.java
  24. 92
      gson/src/main/java/feign/gson/GsonModule.java
  25. 100
      gson/src/test/java/feign/gson/GsonCodecTest.java
  26. 7
      gson/src/test/java/feign/gson/examples/GitHubExample.java
  27. 6
      jackson/README.md
  28. 9
      jackson/src/main/java/feign/jackson/JacksonDecoder.java
  29. 9
      jackson/src/main/java/feign/jackson/JacksonEncoder.java
  30. 103
      jackson/src/main/java/feign/jackson/JacksonModule.java
  31. 70
      jackson/src/test/java/feign/jackson/JacksonCodecTest.java
  32. 8
      jackson/src/test/java/feign/jackson/examples/GitHubExample.java
  33. 8
      jaxb/README.md
  34. 7
      jaxb/src/main/java/feign/jaxb/JAXBDecoder.java
  35. 5
      jaxb/src/main/java/feign/jaxb/JAXBEncoder.java
  36. 66
      jaxb/src/main/java/feign/jaxb/JAXBModule.java
  37. 57
      jaxb/src/test/java/feign/jaxb/JAXBCodecTest.java
  38. 123
      jaxrs/src/main/java/feign/jaxrs/JAXRSContract.java
  39. 133
      jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java
  40. 5
      jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java
  41. 28
      jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java
  42. 12
      ribbon/README.md
  43. 2
      ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java
  44. 24
      ribbon/src/main/java/feign/ribbon/RibbonClient.java
  45. 48
      ribbon/src/main/java/feign/ribbon/RibbonModule.java
  46. 32
      ribbon/src/test/java/feign/ribbon/RibbonClientTest.java
  47. 65
      sax/src/main/java/feign/sax/SAXDecoder.java
  48. 34
      sax/src/test/java/feign/sax/SAXDecoderTest.java

4
CHANGELOG.md

@ -1,3 +1,7 @@
### Version 8.0
* Removes Dagger 1.x Dependency
* Removes support for parameters annotated with `javax.inject.@Named`. Use `feign.@Param` instead.
### Version 7.1 ### Version 7.1
* Introduces feign.@Param to annotate template parameters. Users must migrate from `javax.inject.@Named` to `feign.@Param` before updating to Feign 8.0. * Introduces feign.@Param to annotate template parameters. Users must migrate from `javax.inject.@Named` to `feign.@Param` before updating to Feign 8.0.
* Supports custom expansion via `@Param(value = "name", expander = CustomExpander.class)` * Supports custom expansion via `@Param(value = "name", expander = CustomExpander.class)`

104
README.md

@ -50,35 +50,14 @@ interface Bank {
Bank bank = Feign.builder().decoder(new AccountDecoder()).target(Bank.class, "https://api.examplebank.com"); Bank bank = Feign.builder().decoder(new AccountDecoder()).target(Bank.class, "https://api.examplebank.com");
``` ```
For further flexibility, you can use Dagger modules directly. See the `Dagger` section for more details.
### Request Interceptors
When you need to change all requests, regardless of their target, you'll want to configure a `RequestInterceptor`.
For example, if you are acting as an intermediary, you might want to propagate the `X-Forwarded-For` header.
```java
static class ForwardedForInterceptor implements RequestInterceptor {
@Override public void apply(RequestTemplate template) {
template.header("X-Forwarded-For", "origin.host.com");
}
}
...
Bank bank = Feign.builder().decoder(accountDecoder).requestInterceptor(new ForwardedForInterceptor()).target(Bank.class, "https://api.examplebank.com");
```
Another common example of an interceptor would be authentication, such as using the built-in `BasicAuthRequestInterceptor`.
```java
Bank bank = Feign.builder().decoder(accountDecoder).requestInterceptor(new BasicAuthRequestInterceptor(username, password)).target(Bank.class, "https://api.examplebank.com");
```
### Multiple Interfaces ### Multiple Interfaces
Feign can produce multiple api interfaces. These are defined as `Target<T>` (default `HardCodedTarget<T>`), which allow for dynamic discovery and decoration of requests prior to execution. Feign can produce multiple api interfaces. These are defined as `Target<T>` (default `HardCodedTarget<T>`), which allow for dynamic discovery and decoration of requests prior to execution.
For example, the following pattern might decorate each request with the current url and auth token from the identity service. For example, the following pattern might decorate each request with the current url and auth token from the identity service.
```java ```java
CloudDNS cloudDNS = Feign.builder().target(new CloudIdentityTarget<CloudDNS>(user, apiKey)); Feign feign = Feign.builder().build();
CloudDNS cloudDNS = feign.target(new CloudIdentityTarget<CloudDNS>(user, apiKey));
``` ```
You can find [several examples](https://github.com/Netflix/feign/tree/master/core/src/test/java/feign/examples) in the test tree. Do take time to look at them, as seeing is believing! You can find [several examples](https://github.com/Netflix/feign/tree/master/core/src/test/java/feign/examples) in the test tree. Do take time to look at them, as seeing is believing!
@ -87,7 +66,7 @@ You can find [several examples](https://github.com/Netflix/feign/tree/master/cor
Feign intends to work well within Netflix and other Open Source communities. Modules are welcome to integrate with your favorite projects! Feign intends to work well within Netflix and other Open Source communities. Modules are welcome to integrate with your favorite projects!
### Gson ### Gson
[GsonModule](https://github.com/Netflix/feign/tree/master/gson) adds default encoders and decoders so you get get started with a JSON api. [Gson](https://github.com/Netflix/feign/tree/master/gson) includes an encoder and decoder you can use with a JSON API.
Add `GsonEncoder` and/or `GsonDecoder` to your `Feign.Builder` like so: Add `GsonEncoder` and/or `GsonDecoder` to your `Feign.Builder` like so:
@ -100,7 +79,7 @@ GitHub github = Feign.builder()
``` ```
### Jackson ### Jackson
[JacksonModule](https://github.com/Netflix/feign/tree/master/jackson) adds an encoder and decoder you can use with a JSON API. [Jackson](https://github.com/Netflix/feign/tree/master/jackson) includes an encoder and decoder you can use with a JSON API.
Add `JacksonEncoder` and/or `JacksonDecoder` to your `Feign.Builder` like so: Add `JacksonEncoder` and/or `JacksonDecoder` to your `Feign.Builder` like so:
@ -124,7 +103,7 @@ api = Feign.builder()
``` ```
### JAXB ### JAXB
[JAXBModule](https://github.com/Netflix/feign/tree/master/jaxb) allows you to encode and decode XML using JAXB. [JAXB](https://github.com/Netflix/feign/tree/master/jaxb) includes an encoder and decoder you can use with an XML API.
Add `JAXBEncoder` and/or `JAXBDecoder` to your `Feign.Builder` like so: Add `JAXBEncoder` and/or `JAXBDecoder` to your `Feign.Builder` like so:
@ -136,7 +115,7 @@ api = Feign.builder()
``` ```
### JAX-RS ### JAX-RS
[JAXRSModule](https://github.com/Netflix/feign/tree/master/jaxrs) overrides annotation processing to instead use standard ones supplied by the JAX-RS specification. This is currently targeted at the 1.1 spec. [JAXRSContract](https://github.com/Netflix/feign/tree/master/jaxrs) overrides annotation processing to instead use standard ones supplied by the JAX-RS specification. This is currently targeted at the 1.1 spec.
Here's the example above re-written to use JAX-RS: Here's the example above re-written to use JAX-RS:
```java ```java
@ -147,7 +126,7 @@ interface GitHub {
``` ```
```java ```java
GitHub github = Feign.builder() GitHub github = Feign.builder()
.contract(new JAXRSModule.JAXRSContract()) .contract(new JAXRSContract())
.target(GitHub.class, "https://api.github.com"); .target(GitHub.class, "https://api.github.com");
``` ```
### OkHttp ### OkHttp
@ -162,11 +141,12 @@ GitHub github = Feign.builder()
``` ```
### Ribbon ### Ribbon
[RibbonModule](https://github.com/Netflix/feign/tree/master/ribbon) overrides URL resolution of Feign's client, adding smart routing and resiliency capabilities provided by [Ribbon](https://github.com/Netflix/ribbon). [RibbonClient](https://github.com/Netflix/feign/tree/master/ribbon) overrides URL resolution of Feign's client, adding smart routing and resiliency capabilities provided by [Ribbon](https://github.com/Netflix/ribbon).
Integration requires you to pass your ribbon client name as the host part of the url, for example `myAppProd`. Integration requires you to pass your ribbon client name as the host part of the url, for example `myAppProd`.
```java ```java
MyService api = Feign.create(MyService.class, "https://myAppProd", new RibbonModule()); MyService api = Feign.builder().client(new RibbonClient()).target(MyService.class, "https://myAppProd");
``` ```
### SLF4J ### SLF4J
@ -206,27 +186,45 @@ GitHub github = Feign.builder()
.target(GitHub.class, "https://api.github.com"); .target(GitHub.class, "https://api.github.com");
``` ```
### Advanced usage and Dagger ### Advanced usage
#### Dagger
Feign can be directly wired into Dagger which keeps things at compile time and Android friendly. As opposed to exposing builders for config, Feign intends users to embed their config in Dagger.
Where possible, Feign configuration uses normal Dagger conventions. For example, `RequestInterceptor` bindings are of `Provider.Type.SET`, meaning you can have multiple interceptors. Here's an example of multiple interceptor bindings. #### Logging
You can log the http messages going to and from the target by setting up a `Logger`. Here's the easiest way to do that:
```java ```java
@Provides(type = SET) RequestInterceptor forwardedForInterceptor() { GitHub github = Feign.builder()
return new RequestInterceptor() { .decoder(new GsonDecoder())
@Override public void apply(RequestTemplate template) { .logger(new Logger.JavaLogger().appendToFile("logs/http.log"))
template.header("X-Forwarded-For", "origin.host.com"); .logLevel(Logger.Level.FULL)
} .target(GitHub.class, "https://api.github.com");
}; ```
}
@Provides(type = SET) RequestInterceptor userAgentInterceptor() { The SLF4JLogger (see above) may also be of interest.
return new RequestInterceptor() {
@Override public void apply(RequestTemplate template) {
template.header("User-Agent", "My Cool Client"); #### Request Interceptors
} When you need to change all requests, regardless of their target, you'll want to configure a `RequestInterceptor`.
}; For example, if you are acting as an intermediary, you might want to propagate the `X-Forwarded-For` header.
```java
static class ForwardedForInterceptor implements RequestInterceptor {
@Override public void apply(RequestTemplate template) {
template.header("X-Forwarded-For", "origin.host.com");
}
} }
...
Bank bank = Feign.builder()
.decoder(accountDecoder)
.requestInterceptor(new ForwardedForInterceptor())
.target(Bank.class, "https://api.examplebank.com");
```
Another common example of an interceptor would be authentication, such as using the built-in `BasicAuthRequestInterceptor`.
```java
Bank bank = Feign.builder()
.decoder(accountDecoder)
.requestInterceptor(new BasicAuthRequestInterceptor(username, password))
.target(Bank.class, "https://api.examplebank.com");
``` ```
#### Custom Parameter Expansion #### Custom Parameter Expansion
@ -237,15 +235,3 @@ for example formatting dates.
```java ```java
@RequestLine("GET /?since={date}") Result list(@Param(value = "date", expander = DateToMillis.class) Date date); @RequestLine("GET /?since={date}") Result list(@Param(value = "date", expander = DateToMillis.class) Date date);
``` ```
#### Logging
You can log the http messages going to and from the target by setting up a `Logger`. Here's the easiest way to do that:
```java
GitHub github = Feign.builder()
.decoder(new GsonDecoder())
.logger(new Logger.JavaLogger().appendToFile("logs/http.log"))
.logLevel(Logger.Level.FULL)
.target(GitHub.class, "https://api.github.com");
```
The SLF4JModule (see above) may also be of interest.

1
build.gradle

@ -12,6 +12,5 @@ subprojects {
repositories { repositories {
jcenter() jcenter()
} }
apply from: rootProject.file('dagger.gradle')
group = "com.netflix.${githubProjectName}" // TEMPLATE: Set to organization of project group = "com.netflix.${githubProjectName}" // TEMPLATE: Set to organization of project
} }

3
core/build.gradle

@ -3,9 +3,8 @@ apply plugin: 'java'
sourceCompatibility = 1.6 sourceCompatibility = 1.6
dependencies { dependencies {
testCompile 'com.google.code.gson:gson:2.2.4'
testCompile 'com.fasterxml.jackson.core:jackson-databind:2.2.2'
testCompile 'junit:junit:4.12' testCompile 'junit:junit:4.12'
testCompile 'org.assertj:assertj-core:1.7.1' testCompile 'org.assertj:assertj-core:1.7.1'
testCompile 'com.squareup.okhttp:mockwebserver:2.2.0' testCompile 'com.squareup.okhttp:mockwebserver:2.2.0'
testCompile 'com.google.code.gson:gson:2.2.4' // for example
} }

17
core/src/main/java/feign/Client.java

@ -26,12 +26,10 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.zip.GZIPOutputStream; import java.util.zip.GZIPOutputStream;
import javax.inject.Inject;
import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.SSLSocketFactory;
import dagger.Lazy;
import feign.Request.Options; import feign.Request.Options;
import static feign.Util.CONTENT_ENCODING; import static feign.Util.CONTENT_ENCODING;
@ -55,10 +53,11 @@ public interface Client {
Response execute(Request request, Options options) throws IOException; Response execute(Request request, Options options) throws IOException;
public static class Default implements Client { public static class Default implements Client {
private final Lazy<SSLSocketFactory> sslContextFactory; private final SSLSocketFactory sslContextFactory;
private final Lazy<HostnameVerifier> hostnameVerifier; private final HostnameVerifier hostnameVerifier;
@Inject public Default(Lazy<SSLSocketFactory> sslContextFactory, Lazy<HostnameVerifier> hostnameVerifier) { /** Null parameters imply platform defaults. */
public Default(SSLSocketFactory sslContextFactory, HostnameVerifier hostnameVerifier) {
this.sslContextFactory = sslContextFactory; this.sslContextFactory = sslContextFactory;
this.hostnameVerifier = hostnameVerifier; this.hostnameVerifier = hostnameVerifier;
} }
@ -72,8 +71,12 @@ public interface Client {
final HttpURLConnection connection = (HttpURLConnection) new URL(request.url()).openConnection(); final HttpURLConnection connection = (HttpURLConnection) new URL(request.url()).openConnection();
if (connection instanceof HttpsURLConnection) { if (connection instanceof HttpsURLConnection) {
HttpsURLConnection sslCon = (HttpsURLConnection) connection; HttpsURLConnection sslCon = (HttpsURLConnection) connection;
sslCon.setSSLSocketFactory(sslContextFactory.get()); if (sslContextFactory != null) {
sslCon.setHostnameVerifier(hostnameVerifier.get()); sslCon.setSSLSocketFactory(sslContextFactory);
}
if (hostnameVerifier != null) {
sslCon.setHostnameVerifier(hostnameVerifier);
}
} }
connection.setConnectTimeout(options.connectTimeoutMillis()); connection.setConnectTimeout(options.connectTimeoutMillis());
connection.setReadTimeout(options.readTimeoutMillis()); connection.setReadTimeout(options.readTimeoutMillis());

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

@ -15,13 +15,12 @@
*/ */
package feign; package feign;
import java.util.LinkedHashMap;
import javax.inject.Named;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.net.URI; import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -168,10 +167,9 @@ public interface Contract {
boolean isHttpAnnotation = false; boolean isHttpAnnotation = false;
for (Annotation annotation : annotations) { for (Annotation annotation : annotations) {
Class<? extends Annotation> annotationType = annotation.annotationType(); Class<? extends Annotation> annotationType = annotation.annotationType();
if (annotationType == Param.class || annotationType == Named.class) { if (annotationType == Param.class) {
String name = annotationType == Param.class ? ((Param) annotation).value() : ((Named) annotation).value(); String name = ((Param) annotation).value();
checkState(emptyToNull(name) != null, checkState(emptyToNull(name) != null, "Param annotation was empty on param %s.", paramIndex);
"%s annotation was empty on param %s.", annotationType.getSimpleName(), paramIndex);
nameParam(data, name, paramIndex); nameParam(data, name, paramIndex);
if (annotationType == Param.class) { if (annotationType == Param.class) {
Class<? extends Param.Expander> expander = ((Param) annotation).expander(); Class<? extends Param.Expander> expander = ((Param) annotation).expander();

171
core/src/main/java/feign/Feign.java

@ -15,25 +15,16 @@
*/ */
package feign; package feign;
import dagger.ObjectGraph;
import dagger.Provides;
import feign.Logger.NoOpLogger; import feign.Logger.NoOpLogger;
import feign.ReflectiveFeign.ParseHandlersByName;
import feign.Request.Options; import feign.Request.Options;
import feign.Target.HardCodedTarget; import feign.Target.HardCodedTarget;
import feign.codec.Decoder; import feign.codec.Decoder;
import feign.codec.Encoder; import feign.codec.Encoder;
import feign.codec.ErrorDecoder; import feign.codec.ErrorDecoder;
import javax.inject.Inject;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Set;
/** /**
* Feign's purpose is to ease development against http apis that feign * Feign's purpose is to ease development against http apis that feign
@ -55,80 +46,6 @@ public abstract class Feign {
return new Builder(); return new Builder();
} }
public static <T> T create(Class<T> apiType, String url, Object... modules) {
return create(new HardCodedTarget<T>(apiType, url), modules);
}
/**
* Shortcut to {@link #newInstance(Target) create} a single {@code targeted}
* http api using {@link ReflectiveFeign reflection}.
*/
public static <T> T create(Target<T> target, Object... modules) {
return create(modules).newInstance(target);
}
/**
* Returns a {@link ReflectiveFeign reflective} factory for generating
* {@link Target targeted} http apis.
*/
public static Feign create(Object... modules) {
return ObjectGraph.create(modulesForGraph(modules).toArray()).get(Feign.class);
}
/**
* Returns an {@link ObjectGraph Dagger ObjectGraph} that can inject a
* {@link ReflectiveFeign reflective} Feign.
*/
public static ObjectGraph createObjectGraph(Object... modules) {
return ObjectGraph.create(modulesForGraph(modules).toArray());
}
@SuppressWarnings("rawtypes")
// incomplete as missing Encoder/Decoder
@dagger.Module(injects = {Feign.class, Builder.class}, complete = false, includes = ReflectiveFeign.Module.class)
public static class Defaults {
@Provides Contract contract() {
return new Contract.Default();
}
@Provides Logger.Level logLevel() {
return Logger.Level.NONE;
}
@Provides Logger noOp() {
return new NoOpLogger();
}
@Provides Retryer retryer() {
return new Retryer.Default();
}
@Provides ErrorDecoder errorDecoder() {
return new ErrorDecoder.Default();
}
@Provides Options options() {
return new Options();
}
@Provides SSLSocketFactory sslSocketFactory() {
return SSLSocketFactory.class.cast(SSLSocketFactory.getDefault());
}
@Provides HostnameVerifier hostnameVerifier() {
return HttpsURLConnection.getDefaultHostnameVerifier();
}
@Provides Client httpClient(Client.Default client) {
return client;
}
@Provides InvocationHandlerFactory invocationHandlerFactory() {
return new InvocationHandlerFactory.Default();
}
}
/** /**
* <br> * <br>
* Configuration keys are formatted as unresolved <a href= * Configuration keys are formatted as unresolved <a href=
@ -160,32 +77,18 @@ public abstract class Feign {
return builder.append(')').toString(); return builder.append(')').toString();
} }
private static List<Object> modulesForGraph(Object... modules) {
List<Object> modulesForGraph = new ArrayList<Object>(2);
modulesForGraph.add(new Defaults());
if (modules != null)
for (Object module : modules)
modulesForGraph.add(module);
return modulesForGraph;
}
@dagger.Module(injects = Feign.class, includes = ReflectiveFeign.Module.class)
public static class Builder { public static class Builder {
private final Set<RequestInterceptor> requestInterceptors = new LinkedHashSet<RequestInterceptor>(); private final List<RequestInterceptor> requestInterceptors = new ArrayList<RequestInterceptor>();
@Inject Logger.Level logLevel; private Logger.Level logLevel = Logger.Level.NONE;
@Inject Contract contract; private Contract contract = new Contract.Default();
@Inject Client client; private Client client = new Client.Default(null, null);
@Inject Retryer retryer; private Retryer retryer = new Retryer.Default();
@Inject Logger logger; private Logger logger = new NoOpLogger();
Encoder encoder = new Encoder.Default(); private Encoder encoder = new Encoder.Default();
Decoder decoder = new Decoder.Default(); private Decoder decoder = new Decoder.Default();
@Inject ErrorDecoder errorDecoder; private ErrorDecoder errorDecoder = new ErrorDecoder.Default();
@Inject Options options; private Options options = new Options();
@Inject InvocationHandlerFactory invocationHandlerFactory; private InvocationHandlerFactory invocationHandlerFactory = new InvocationHandlerFactory.Default();
Builder() {
ObjectGraph.create(new Defaults()).inject(this);
}
public Builder logLevel(Logger.Level logLevel) { public Builder logLevel(Logger.Level logLevel) {
this.logLevel = logLevel; this.logLevel = logLevel;
@ -262,51 +165,15 @@ public abstract class Feign {
} }
public <T> T target(Target<T> target) { public <T> T target(Target<T> target) {
return ObjectGraph.create(this).get(Feign.class).newInstance(target); return build().newInstance(target);
}
@Provides Logger.Level logLevel() {
return logLevel;
}
@Provides Contract contract() {
return contract;
}
@Provides Client client() {
return client;
}
@Provides Retryer retryer() {
return retryer;
}
@Provides Logger logger() {
return logger;
}
@Provides Encoder encoder() {
return encoder;
}
@Provides Decoder decoder() {
return decoder;
}
@Provides ErrorDecoder errorDecoder() {
return errorDecoder;
}
@Provides Options options() {
return options;
}
@Provides(type = Provides.Type.SET_VALUES) Set<RequestInterceptor> requestInterceptors() {
return requestInterceptors;
} }
@Provides InvocationHandlerFactory invocationHandlerFactory() { public Feign build() {
return invocationHandlerFactory; SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger, logLevel);
ParseHandlersByName handlersByName = new ParseHandlersByName( contract, options, encoder, decoder,
errorDecoder, synchronousMethodHandlerFactory);
return new ReflectiveFeign(handlersByName, invocationHandlerFactory);
} }
} }
} }

1
core/src/main/java/feign/InvocationHandlerFactory.java

@ -21,6 +21,7 @@ import java.util.Map;
/** Controls reflective method dispatch. */ /** Controls reflective method dispatch. */
public interface InvocationHandlerFactory { public interface InvocationHandlerFactory {
/** Like {@link InvocationHandler#invoke(Object, java.lang.reflect.Method, Object[])}, except for a single method. */ /** Like {@link InvocationHandler#invoke(Object, java.lang.reflect.Method, Object[])}, except for a single method. */
interface MethodHandler { interface MethodHandler {
Object invoke(Object[] argv) throws Throwable; Object invoke(Object[] argv) throws Throwable;

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

@ -15,7 +15,6 @@
*/ */
package feign; package feign;
import dagger.Provides;
import feign.InvocationHandlerFactory.MethodHandler; import feign.InvocationHandlerFactory.MethodHandler;
import feign.Param.Expander; import feign.Param.Expander;
import feign.Request.Options; import feign.Request.Options;
@ -24,28 +23,24 @@ import feign.codec.EncodeException;
import feign.codec.Encoder; import feign.codec.Encoder;
import feign.codec.ErrorDecoder; import feign.codec.ErrorDecoder;
import javax.inject.Inject;
import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Proxy; import java.lang.reflect.Proxy;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Set;
import static feign.Util.checkArgument; import static feign.Util.checkArgument;
import static feign.Util.checkNotNull; import static feign.Util.checkNotNull;
@SuppressWarnings("rawtypes")
public class ReflectiveFeign extends Feign { public class ReflectiveFeign extends Feign {
private final ParseHandlersByName targetToHandlersByName; private final ParseHandlersByName targetToHandlersByName;
private final InvocationHandlerFactory factory; private final InvocationHandlerFactory factory;
@Inject ReflectiveFeign(ParseHandlersByName targetToHandlersByName, InvocationHandlerFactory factory) { ReflectiveFeign(ParseHandlersByName targetToHandlersByName, InvocationHandlerFactory factory) {
this.targetToHandlersByName = targetToHandlersByName; this.targetToHandlersByName = targetToHandlersByName;
this.factory = factory; this.factory = factory;
} }
@ -109,17 +104,6 @@ public class ReflectiveFeign extends Feign {
} }
} }
@dagger.Module(complete = false, injects = {Feign.class, SynchronousMethodHandler.Factory.class}, library = true)
public static class Module {
@Provides(type = Provides.Type.SET_VALUES) Set<RequestInterceptor> noRequestInterceptors() {
return Collections.emptySet();
}
@Provides Feign provideFeign(ReflectiveFeign in) {
return in;
}
}
static final class ParseHandlersByName { static final class ParseHandlersByName {
private final Contract contract; private final Contract contract;
private final Options options; private final Options options;
@ -128,9 +112,8 @@ public class ReflectiveFeign extends Feign {
private final ErrorDecoder errorDecoder; private final ErrorDecoder errorDecoder;
private final SynchronousMethodHandler.Factory factory; private final SynchronousMethodHandler.Factory factory;
@SuppressWarnings("unchecked") ParseHandlersByName(Contract contract, Options options, Encoder encoder, Decoder decoder,
@Inject ParseHandlersByName(Contract contract, Options options, Encoder encoder, Decoder decoder, ErrorDecoder errorDecoder, SynchronousMethodHandler.Factory factory) {
ErrorDecoder errorDecoder, SynchronousMethodHandler.Factory factory) {
this.contract = contract; this.contract = contract;
this.options = options; this.options = options;
this.factory = factory; this.factory = factory;

14
core/src/main/java/feign/RequestInterceptor.java

@ -33,19 +33,7 @@ package feign;
* <br> * <br>
* <br><b>Configuration</b><br> * <br><b>Configuration</b><br>
* <br> * <br>
* {@code RequestInterceptors} are configured via Dagger * {@code RequestInterceptors} are configured via {@link Feign.Builder#requestInterceptors}.
* {@link dagger.Provides.Type#SET set} or
* {@link dagger.Provides.Type#SET_VALUES set values}
* {@link dagger.Provides provider} methods.
* <br>
* <br>
* For example:
* <br>
* <pre>
* {@literal @}Provides(Type = SET) RequestInterceptor addTimestamp(TimestampInterceptor in) {
* return in;
* }
* </pre>
* <br> * <br>
* <br><b>Implementation notes</b><br> * <br><b>Implementation notes</b><br>
* <br> * <br>

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

@ -20,11 +20,8 @@ import feign.Request.Options;
import feign.codec.DecodeException; import feign.codec.DecodeException;
import feign.codec.Decoder; import feign.codec.Decoder;
import feign.codec.ErrorDecoder; import feign.codec.ErrorDecoder;
import javax.inject.Inject;
import javax.inject.Provider;
import java.io.IOException; import java.io.IOException;
import java.util.Set; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static feign.FeignException.errorExecuting; import static feign.FeignException.errorExecuting;
@ -37,13 +34,13 @@ final class SynchronousMethodHandler implements MethodHandler {
static class Factory { static class Factory {
private final Client client; private final Client client;
private final Provider<Retryer> retryer; private final Retryer retryer;
private final Set<RequestInterceptor> requestInterceptors; private final List<RequestInterceptor> requestInterceptors;
private final Logger logger; private final Logger logger;
private final Provider<Logger.Level> logLevel; private final Logger.Level logLevel;
@Inject Factory(Client client, Provider<Retryer> retryer, Set<RequestInterceptor> requestInterceptors, Factory(Client client, Retryer retryer, List<RequestInterceptor> requestInterceptors,
Logger logger, Provider<Logger.Level> logLevel) { Logger logger, Logger.Level logLevel) {
this.client = checkNotNull(client, "client"); this.client = checkNotNull(client, "client");
this.retryer = checkNotNull(retryer, "retryer"); this.retryer = checkNotNull(retryer, "retryer");
this.requestInterceptors = checkNotNull(requestInterceptors, "requestInterceptors"); this.requestInterceptors = checkNotNull(requestInterceptors, "requestInterceptors");
@ -61,18 +58,18 @@ final class SynchronousMethodHandler implements MethodHandler {
private final MethodMetadata metadata; private final MethodMetadata metadata;
private final Target<?> target; private final Target<?> target;
private final Client client; private final Client client;
private final Provider<Retryer> retryer; private final Retryer retryer;
private final Set<RequestInterceptor> requestInterceptors; private final List<RequestInterceptor> requestInterceptors;
private final Logger logger; private final Logger logger;
private final Provider<Logger.Level> logLevel; private final Logger.Level logLevel;
private final RequestTemplate.Factory buildTemplateFromArgs; private final RequestTemplate.Factory buildTemplateFromArgs;
private final Options options; private final Options options;
private final Decoder decoder; private final Decoder decoder;
private final ErrorDecoder errorDecoder; private final ErrorDecoder errorDecoder;
private SynchronousMethodHandler(Target<?> target, Client client, Provider<Retryer> retryer, private SynchronousMethodHandler(Target<?> target, Client client, Retryer retryer,
Set<RequestInterceptor> requestInterceptors, Logger logger, List<RequestInterceptor> requestInterceptors, Logger logger,
Provider<Logger.Level> logLevel, MethodMetadata metadata, Logger.Level logLevel, MethodMetadata metadata,
RequestTemplate.Factory buildTemplateFromArgs, Options options, RequestTemplate.Factory buildTemplateFromArgs, Options options,
Decoder decoder, ErrorDecoder errorDecoder) { Decoder decoder, ErrorDecoder errorDecoder) {
this.target = checkNotNull(target, "target"); this.target = checkNotNull(target, "target");
@ -90,14 +87,14 @@ final class SynchronousMethodHandler implements MethodHandler {
@Override public Object invoke(Object[] argv) throws Throwable { @Override public Object invoke(Object[] argv) throws Throwable {
RequestTemplate template = buildTemplateFromArgs.create(argv); RequestTemplate template = buildTemplateFromArgs.create(argv);
Retryer retryer = this.retryer.get(); Retryer retryer = this.retryer;
while (true) { while (true) {
try { try {
return executeAndDecode(template); return executeAndDecode(template);
} catch (RetryableException e) { } catch (RetryableException e) {
retryer.continueOrPropagate(e); retryer.continueOrPropagate(e);
if (logLevel.get() != Logger.Level.NONE) { if (logLevel != Logger.Level.NONE) {
logger.logRetry(metadata.configKey(), logLevel.get()); logger.logRetry(metadata.configKey(), logLevel);
} }
continue; continue;
} }
@ -107,8 +104,8 @@ final class SynchronousMethodHandler implements MethodHandler {
Object executeAndDecode(RequestTemplate template) throws Throwable { Object executeAndDecode(RequestTemplate template) throws Throwable {
Request request = targetRequest(template); Request request = targetRequest(template);
if (logLevel.get() != Logger.Level.NONE) { if (logLevel != Logger.Level.NONE) {
logger.logRequest(metadata.configKey(), logLevel.get(), request); logger.logRequest(metadata.configKey(), logLevel, request);
} }
Response response; Response response;
@ -116,16 +113,16 @@ final class SynchronousMethodHandler implements MethodHandler {
try { try {
response = client.execute(request, options); response = client.execute(request, options);
} catch (IOException e) { } catch (IOException e) {
if (logLevel.get() != Logger.Level.NONE) { if (logLevel != Logger.Level.NONE) {
logger.logIOException(metadata.configKey(), logLevel.get(), e, elapsedTime(start)); logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
} }
throw errorExecuting(request, e); throw errorExecuting(request, e);
} }
long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
try { try {
if (logLevel.get() != Logger.Level.NONE) { if (logLevel != Logger.Level.NONE) {
response = logger.logAndRebufferResponse(metadata.configKey(), logLevel.get(), response, elapsedTime); response = logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime);
} }
if (response.status() >= 200 && response.status() < 300) { if (response.status() >= 200 && response.status() < 300) {
if (Response.class == metadata.returnType()) { if (Response.class == metadata.returnType()) {
@ -144,8 +141,8 @@ final class SynchronousMethodHandler implements MethodHandler {
throw errorDecoder.decode(metadata.configKey(), response); throw errorDecoder.decode(metadata.configKey(), response);
} }
} catch (IOException e) { } catch (IOException e) {
if (logLevel.get() != Logger.Level.NONE) { if (logLevel != Logger.Level.NONE) {
logger.logIOException(metadata.configKey(), logLevel.get(), e, elapsedTime); logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime);
} }
throw errorReading(request, response, e); throw errorReading(request, response, e);
} finally { } finally {

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

@ -19,7 +19,6 @@ import com.google.gson.reflect.TypeToken;
import java.net.URI; import java.net.URI;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import javax.inject.Named;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
@ -264,70 +263,4 @@ public class DefaultContractTest {
assertThat(md.indexToExpanderClass()) assertThat(md.indexToExpanderClass())
.containsExactly(entry(0, DateToMillis.class)); .containsExactly(entry(0, DateToMillis.class));
} }
// TODO: remove all of below in 8.x
interface WithPathAndQueryParamsAnnotatedWithNamed {
@RequestLine("GET /domains/{domainId}/records?name={name}&type={type}")
Response recordsByNameAndType(@Named("domainId") int id, @Named("name") String nameFilter,
@Named("type") String typeFilter);
}
@Test public void pathAndQueryParamsAnnotatedWithNamed() throws Exception {
MethodMetadata md = contract.parseAndValidatateMetadata(WithPathAndQueryParamsAnnotatedWithNamed.class.getDeclaredMethod
("recordsByNameAndType", int.class, String.class, String.class));
assertThat(md.template())
.hasQueries(entry("name", asList("{name}")), entry("type", asList("{type}")));
assertThat(md.indexToName()).containsExactly(
entry(0, asList("domainId")),
entry(1, asList("name")),
entry(2, asList("type"))
);
}
interface FormParamsAnnotatedWithNamed {
@RequestLine("POST /")
@Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D")
void login(
@Named("customer_name") String customer,
@Named("user_name") String user, @Named("password") String password);
}
@Test public void bodyWithTemplateAnnotatedWithNamed() throws Exception {
MethodMetadata md = contract.parseAndValidatateMetadata(FormParamsAnnotatedWithNamed.class.getDeclaredMethod("login", String.class,
String.class, String.class));
assertThat(md.template())
.hasBodyTemplate("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D");
}
@Test public void formParamsAnnotatedWithNamedParseIntoIndexToName() throws Exception {
MethodMetadata md = contract.parseAndValidatateMetadata(FormParamsAnnotatedWithNamed.class.getDeclaredMethod("login", String.class,
String.class, String.class));
assertThat(md.formParams())
.containsExactly("customer_name", "user_name", "password");
assertThat(md.indexToName()).containsExactly(
entry(0, asList("customer_name")),
entry(1, asList("user_name")),
entry(2, asList("password"))
);
}
interface HeaderParamsAnnotatedWithNamed {
@RequestLine("POST /")
@Headers("Auth-Token: {Auth-Token}") void logout(@Named("Auth-Token") String token);
}
@Test public void headerParamsAnnotatedWithNamedParseIntoIndexToName() throws Exception {
MethodMetadata md = contract.parseAndValidatateMetadata(HeaderParamsAnnotatedWithNamed.class.getDeclaredMethod("logout", String.class));
assertThat(md.template()).hasHeaders(entry("Auth-Token", asList("{Auth-Token}")));
assertThat(md.indexToName())
.containsExactly(entry(0, asList("Auth-Token")));
}
} }

233
core/src/test/java/feign/FeignTest.java

@ -19,8 +19,6 @@ import com.google.gson.Gson;
import com.squareup.okhttp.mockwebserver.MockResponse; import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.SocketPolicy; import com.squareup.okhttp.mockwebserver.SocketPolicy;
import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule; import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
import dagger.Module;
import dagger.Provides;
import feign.Target.HardCodedTarget; import feign.Target.HardCodedTarget;
import feign.codec.Decoder; import feign.codec.Decoder;
import feign.codec.Encoder; import feign.codec.Encoder;
@ -33,19 +31,15 @@ import java.util.Arrays;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import javax.inject.Singleton;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
import static dagger.Provides.Type.SET;
import static feign.Util.UTF_8; import static feign.Util.UTF_8;
import static feign.assertj.MockWebServerAssertions.assertThat; import static feign.assertj.MockWebServerAssertions.assertThat;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
// unbound wildcards are not currently injectable in dagger.
@SuppressWarnings("rawtypes")
public class FeignTest { public class FeignTest {
@Rule public final ExpectedException thrown = ExpectedException.none(); @Rule public final ExpectedException thrown = ExpectedException.none();
@Rule public final MockWebServerRule server = new MockWebServerRule(); @Rule public final MockWebServerRule server = new MockWebServerRule();
@ -78,32 +72,12 @@ public class FeignTest {
return String.valueOf(((Date) value).getTime()); return String.valueOf(((Date) value).getTime());
} }
} }
@dagger.Module(injects = Feign.class, addsTo = Feign.Defaults.class)
static class Module {
@Provides Decoder defaultDecoder() {
return new Decoder.Default();
}
@Provides Encoder defaultEncoder() {
return new Encoder() {
@Override public void encode(Object object, RequestTemplate template) {
if (object instanceof Map) {
template.body(new Gson().toJson(object));
} else {
template.body(object.toString());
}
}
};
}
}
} }
@Test public void iterableQueryParams() throws IOException, InterruptedException { @Test public void iterableQueryParams() throws IOException, InterruptedException {
server.enqueue(new MockResponse().setBody("foo")); server.enqueue(new MockResponse().setBody("foo"));
TestInterface api = TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort());
Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module());
api.queryParams("user", Arrays.asList("apple", "pear")); api.queryParams("user", Arrays.asList("apple", "pear"));
@ -119,12 +93,10 @@ public class FeignTest {
@RequestLine("POST /") void binaryRequestBody(byte[] contents); @RequestLine("POST /") void binaryRequestBody(byte[] contents);
} }
@Test @Test public void postTemplateParamsResolve() throws IOException, InterruptedException {
public void postTemplateParamsResolve() throws IOException, InterruptedException {
server.enqueue(new MockResponse().setBody("foo")); server.enqueue(new MockResponse().setBody("foo"));
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort());
new TestInterface.Module());
api.login("netflix", "denominator", "password"); api.login("netflix", "denominator", "password");
@ -132,24 +104,20 @@ public class FeignTest {
.hasBody("{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}"); .hasBody("{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}");
} }
@Test @Test public void responseCoercesToStringBody() throws IOException, InterruptedException {
public void responseCoercesToStringBody() throws IOException, InterruptedException {
server.enqueue(new MockResponse().setBody("foo")); server.enqueue(new MockResponse().setBody("foo"));
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort());
new TestInterface.Module());
Response response = api.response(); Response response = api.response();
assertTrue(response.body().isRepeatable()); assertTrue(response.body().isRepeatable());
assertEquals("foo", response.body().toString()); assertEquals("foo", response.body().toString());
} }
@Test @Test public void postFormParams() throws IOException, InterruptedException {
public void postFormParams() throws IOException, InterruptedException {
server.enqueue(new MockResponse().setBody("foo")); server.enqueue(new MockResponse().setBody("foo"));
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort());
new TestInterface.Module());
api.form("netflix", "denominator", "password"); api.form("netflix", "denominator", "password");
@ -157,12 +125,10 @@ public class FeignTest {
.hasBody("{\"customer_name\":\"netflix\",\"user_name\":\"denominator\",\"password\":\"password\"}"); .hasBody("{\"customer_name\":\"netflix\",\"user_name\":\"denominator\",\"password\":\"password\"}");
} }
@Test @Test public void postBodyParam() throws IOException, InterruptedException {
public void postBodyParam() throws IOException, InterruptedException {
server.enqueue(new MockResponse().setBody("foo")); server.enqueue(new MockResponse().setBody("foo"));
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort());
new TestInterface.Module());
api.body(Arrays.asList("netflix", "denominator", "password")); api.body(Arrays.asList("netflix", "denominator", "password"));
@ -171,12 +137,10 @@ public class FeignTest {
.hasBody("[netflix, denominator, password]"); .hasBody("[netflix, denominator, password]");
} }
@Test @Test public void postGZIPEncodedBodyParam() throws IOException, InterruptedException {
public void postGZIPEncodedBodyParam() throws IOException, InterruptedException {
server.enqueue(new MockResponse().setBody("foo")); server.enqueue(new MockResponse().setBody("foo"));
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort());
new TestInterface.Module());
api.gzipBody(Arrays.asList("netflix", "denominator", "password")); api.gzipBody(Arrays.asList("netflix", "denominator", "password"));
@ -185,23 +149,18 @@ public class FeignTest {
.hasGzippedBody("[netflix, denominator, password]".getBytes(UTF_8)); .hasGzippedBody("[netflix, denominator, password]".getBytes(UTF_8));
} }
@Module(library = true)
static class ForwardedForInterceptor implements RequestInterceptor { static class ForwardedForInterceptor implements RequestInterceptor {
@Provides(type = SET) RequestInterceptor provideThis() {
return this;
}
@Override public void apply(RequestTemplate template) { @Override public void apply(RequestTemplate template) {
template.header("X-Forwarded-For", "origin.host.com"); template.header("X-Forwarded-For", "origin.host.com");
} }
} }
@Test @Test public void singleInterceptor() throws IOException, InterruptedException {
public void singleInterceptor() throws IOException, InterruptedException {
server.enqueue(new MockResponse().setBody("foo")); server.enqueue(new MockResponse().setBody("foo"));
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), TestInterface api = new TestInterfaceBuilder()
new TestInterface.Module(), new ForwardedForInterceptor()); .requestInterceptor(new ForwardedForInterceptor())
.target("http://localhost:" + server.getPort());
api.post(); api.post();
@ -209,35 +168,29 @@ public class FeignTest {
.hasHeaders("X-Forwarded-For: origin.host.com"); .hasHeaders("X-Forwarded-For: origin.host.com");
} }
@Module(library = true)
static class UserAgentInterceptor implements RequestInterceptor { static class UserAgentInterceptor implements RequestInterceptor {
@Provides(type = SET) RequestInterceptor provideThis() {
return this;
}
@Override public void apply(RequestTemplate template) { @Override public void apply(RequestTemplate template) {
template.header("User-Agent", "Feign"); template.header("User-Agent", "Feign");
} }
} }
@Test @Test public void multipleInterceptor() throws IOException, InterruptedException {
public void multipleInterceptor() throws IOException, InterruptedException {
server.enqueue(new MockResponse().setBody("foo")); server.enqueue(new MockResponse().setBody("foo"));
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), TestInterface api = new TestInterfaceBuilder()
new TestInterface.Module(), new ForwardedForInterceptor(), new UserAgentInterceptor()); .requestInterceptor(new ForwardedForInterceptor())
.requestInterceptor(new UserAgentInterceptor())
.target("http://localhost:" + server.getPort());
api.post(); api.post();
assertThat(server.takeRequest()) assertThat(server.takeRequest()).hasHeaders("X-Forwarded-For: origin.host.com", "User-Agent: Feign");
.hasHeaders("X-Forwarded-For: origin.host.com", "User-Agent: Feign");
} }
@Test public void customExpander() throws Exception { @Test public void customExpander() throws Exception {
server.enqueue(new MockResponse()); server.enqueue(new MockResponse());
TestInterface api = TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort());
Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module());
api.expand(new Date(1234l)); api.expand(new Date(1234l));
@ -251,30 +204,21 @@ public class FeignTest {
Feign.configKey(TestInterface.class.getDeclaredMethod("uriParam", String.class, URI.class, String.class))); Feign.configKey(TestInterface.class.getDeclaredMethod("uriParam", String.class, URI.class, String.class)));
} }
@dagger.Module(overrides = true, library = true, includes = TestInterface.Module.class) static class IllegalArgumentExceptionOn404 extends ErrorDecoder.Default {
static class IllegalArgumentExceptionOn404 { @Override public Exception decode(String methodKey, Response response) {
@Provides @Singleton ErrorDecoder errorDecoder() { if (response.status() == 404) return new IllegalArgumentException("zone not found");
return new ErrorDecoder.Default() { return super.decode(methodKey, response);
@Override
public Exception decode(String methodKey, Response response) {
if (response.status() == 404)
return new IllegalArgumentException("zone not found");
return super.decode(methodKey, response);
}
};
} }
} }
@Test @Test public void canOverrideErrorDecoder() throws IOException, InterruptedException {
public void canOverrideErrorDecoder() throws IOException, InterruptedException {
server.enqueue(new MockResponse().setResponseCode(404).setBody("foo")); server.enqueue(new MockResponse().setResponseCode(404).setBody("foo"));
thrown.expect(IllegalArgumentException.class); thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("zone not found"); thrown.expectMessage("zone not found");
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), TestInterface api = new TestInterfaceBuilder()
new IllegalArgumentExceptionOn404()); .errorDecoder(new IllegalArgumentExceptionOn404())
.target("http://localhost:" + server.getPort());
api.post(); api.post();
} }
@ -283,83 +227,58 @@ public class FeignTest {
server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START));
server.enqueue(new MockResponse().setBody("success!")); server.enqueue(new MockResponse().setBody("success!"));
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort());
new TestInterface.Module());
api.post(); api.post();
assertEquals(2, server.getRequestCount()); assertEquals(2, server.getRequestCount());
} }
@dagger.Module(overrides = true, library = true, includes = TestInterface.Module.class)
static class DecodeFail {
@Provides Decoder decoder() {
return new Decoder() {
@Override
public Object decode(Response response, Type type) {
return "fail";
}
};
}
}
@Test public void overrideTypeSpecificDecoder() throws IOException, InterruptedException { @Test public void overrideTypeSpecificDecoder() throws IOException, InterruptedException {
server.enqueue(new MockResponse().setBody("success!")); server.enqueue(new MockResponse().setBody("success!"));
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), TestInterface api = new TestInterfaceBuilder()
new DecodeFail()); .decoder(new Decoder() {
@Override public Object decode(Response response, Type type) {
return "fail";
}
}).target("http://localhost:" + server.getPort());
assertEquals(api.post(), "fail"); assertEquals(api.post(), "fail");
} }
@dagger.Module(overrides = true, library = true, includes = TestInterface.Module.class)
static class RetryableExceptionOnRetry {
@Provides Decoder decoder() {
return new StringDecoder() {
@Override
public Object decode(Response response, Type type) throws IOException, FeignException {
String string = super.decode(response, type).toString();
if ("retry!".equals(string))
throw new RetryableException(string, null);
return string;
}
};
}
}
/** /**
* when you must parse a 2xx status to determine if the operation succeeded or not. * when you must parse a 2xx status to determine if the operation succeeded or not.
*/ */
public void retryableExceptionInDecoder() throws IOException, InterruptedException { @Test public void retryableExceptionInDecoder() throws IOException, InterruptedException {
server.enqueue(new MockResponse().setBody("retry!")); server.enqueue(new MockResponse().setBody("retry!"));
server.enqueue(new MockResponse().setBody("success!")); server.enqueue(new MockResponse().setBody("success!"));
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), TestInterface api = new TestInterfaceBuilder()
new RetryableExceptionOnRetry()); .decoder(new StringDecoder() {
@Override public Object decode(Response response, Type type) throws IOException {
String string = super.decode(response, type).toString();
if ("retry!".equals(string)) throw new RetryableException(string, null);
return string;
}
}).target("http://localhost:" + server.getPort());
assertEquals(api.post(), "success!"); assertEquals(api.post(), "success!");
assertEquals(2, server.getRequestCount()); assertEquals(2, server.getRequestCount());
} }
@dagger.Module(overrides = true, library = true, includes = TestInterface.Module.class)
static class IOEOnDecode {
@Provides Decoder decoder() {
return new Decoder() {
@Override
public Object decode(Response response, Type type) throws IOException {
throw new IOException("error reading response");
}
};
}
}
@Test @Test public void doesntRetryAfterResponseIsSent() throws IOException, InterruptedException {
public void doesntRetryAfterResponseIsSent() throws IOException, InterruptedException {
server.enqueue(new MockResponse().setBody("success!")); server.enqueue(new MockResponse().setBody("success!"));
thrown.expect(FeignException.class); thrown.expect(FeignException.class);
thrown.expectMessage("error reading response POST http://"); thrown.expectMessage("error reading response POST http://");
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new IOEOnDecode()); TestInterface api = new TestInterfaceBuilder()
.decoder(new Decoder() {
@Override public Object decode(Response response, Type type) throws IOException {
throw new IOException("error reading response");
}
}).target("http://localhost:" + server.getPort());
try { try {
api.post(); api.post();
@ -424,4 +343,42 @@ public class FeignTest {
assertThat(server.takeRequest()) assertThat(server.takeRequest())
.hasBody(expectedRequest); .hasBody(expectedRequest);
} }
static final class TestInterfaceBuilder {
private final Feign.Builder delegate = new Feign.Builder()
.decoder(new Decoder.Default())
.encoder(new Encoder() {
@Override public void encode(Object object, RequestTemplate template) {
if (object instanceof Map) {
template.body(new Gson().toJson(object));
} else {
template.body(object.toString());
}
}
});
TestInterfaceBuilder requestInterceptor(RequestInterceptor requestInterceptor) {
delegate.requestInterceptor(requestInterceptor);
return this;
}
TestInterfaceBuilder client(Client client) {
delegate.client(client);
return this;
}
TestInterfaceBuilder decoder(Decoder decoder) {
delegate.decoder(decoder);
return this;
}
TestInterfaceBuilder errorDecoder(ErrorDecoder errorDecoder) {
delegate.errorDecoder(errorDecoder);
return this;
}
TestInterface target(String url) {
return delegate.target(TestInterface.class, url);
}
}
} }

2
core/src/test/java/feign/LoggerTest.java

@ -192,7 +192,6 @@ public class LoggerTest {
} }
@Test public void unknownHostEmits() throws IOException, InterruptedException { @Test public void unknownHostEmits() throws IOException, InterruptedException {
SendsStuff api = Feign.builder() SendsStuff api = Feign.builder()
.logger(logger) .logger(logger)
.logLevel(logLevel) .logLevel(logLevel)
@ -232,7 +231,6 @@ public class LoggerTest {
} }
@Test public void retryEmits() throws IOException, InterruptedException { @Test public void retryEmits() throws IOException, InterruptedException {
thrown.expect(FeignException.class); thrown.expect(FeignException.class);
SendsStuff api = Feign.builder() SendsStuff api = Feign.builder()

28
core/src/test/java/feign/client/DefaultClientTest.java

@ -18,7 +18,6 @@ package feign.client;
import com.squareup.okhttp.mockwebserver.MockResponse; import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.SocketPolicy; import com.squareup.okhttp.mockwebserver.SocketPolicy;
import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule; import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
import dagger.Lazy;
import feign.Client; import feign.Client;
import feign.Feign; import feign.Feign;
import feign.FeignException; import feign.FeignException;
@ -29,9 +28,7 @@ import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.net.ProtocolException; import java.net.ProtocolException;
import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
@ -98,15 +95,7 @@ public class DefaultClientTest {
api.patch(); api.patch();
} }
Client trustSSLSockets = new Client.Default(new Lazy<SSLSocketFactory>() { Client trustSSLSockets = new Client.Default(TrustingSSLSocketFactory.get(), null);
@Override public SSLSocketFactory get() {
return TrustingSSLSocketFactory.get();
}
}, new Lazy<HostnameVerifier>() {
@Override public HostnameVerifier get() {
return HttpsURLConnection.getDefaultHostnameVerifier();
}
});
@Test public void canOverrideSSLSocketFactory() throws IOException, InterruptedException { @Test public void canOverrideSSLSocketFactory() throws IOException, InterruptedException {
server.get().useHttps(TrustingSSLSocketFactory.get("localhost"), false); server.get().useHttps(TrustingSSLSocketFactory.get("localhost"), false);
@ -119,18 +108,9 @@ public class DefaultClientTest {
api.post("foo"); api.post("foo");
} }
Client disableHostnameVerification = new Client.Default(new Lazy<SSLSocketFactory>() { Client disableHostnameVerification = new Client.Default(TrustingSSLSocketFactory.get(), new HostnameVerifier() {
@Override public SSLSocketFactory get() { @Override public boolean verify(String s, SSLSession sslSession) {
return TrustingSSLSocketFactory.get(); return true;
}
}, new Lazy<HostnameVerifier>() {
@Override public HostnameVerifier get() {
return new HostnameVerifier() {
@Override
public boolean verify(String s, SSLSession sslSession) {
return true;
}
};
} }
}); });

6
core/src/test/java/feign/client/TrustingSSLSocketFactory.java

@ -36,8 +36,6 @@ import javax.net.ssl.TrustManager;
import javax.net.ssl.X509KeyManager; import javax.net.ssl.X509KeyManager;
import javax.net.ssl.X509TrustManager; import javax.net.ssl.X509TrustManager;
import static com.google.common.base.Throwables.propagate;
/** /**
* Used for ssl tests to simplify setup. * Used for ssl tests to simplify setup.
*/ */
@ -69,7 +67,7 @@ final class TrustingSSLSocketFactory extends SSLSocketFactory implements X509Tru
sc.init(new KeyManager[]{this}, new TrustManager[]{this}, new SecureRandom()); sc.init(new KeyManager[]{this}, new TrustManager[]{this}, new SecureRandom());
this.delegate = sc.getSocketFactory(); this.delegate = sc.getSocketFactory();
} catch (Exception e) { } catch (Exception e) {
throw propagate(e); throw new RuntimeException(e);
} }
this.serverAlias = serverAlias; this.serverAlias = serverAlias;
if (serverAlias.isEmpty()) { if (serverAlias.isEmpty()) {
@ -82,7 +80,7 @@ final class TrustingSSLSocketFactory extends SSLSocketFactory implements X509Tru
Certificate[] rawChain = keyStore.getCertificateChain(serverAlias); Certificate[] rawChain = keyStore.getCertificateChain(serverAlias);
this.certificateChain = Arrays.copyOf(rawChain, rawChain.length, X509Certificate[].class); this.certificateChain = Arrays.copyOf(rawChain, rawChain.length, X509Certificate[].class);
} catch (Exception e) { } catch (Exception e) {
throw propagate(e); throw new RuntimeException(e);
} }
} }
} }

2
core/src/test/java/feign/examples/GitHubExample.java

@ -48,9 +48,9 @@ public class GitHubExample {
public static void main(String... args) { public static void main(String... args) {
GitHub github = Feign.builder() GitHub github = Feign.builder()
.decoder(new GsonDecoder())
.logger(new Logger.ErrorLogger()) .logger(new Logger.ErrorLogger())
.logLevel(Logger.Level.BASIC) .logLevel(Logger.Level.BASIC)
.decoder(new GsonDecoder())
.target(GitHub.class, "https://api.github.com"); .target(GitHub.class, "https://api.github.com");
System.out.println("Let's fetch and print a list of the contributors to this library."); System.out.println("Let's fetch and print a list of the contributors to this library.");

178
dagger.gradle

@ -1,178 +0,0 @@
// Manages classpath and IDE annotation processing config for dagger.
//
// setup:
// Add the following to your root build.gradle
//
// apply plugin: 'idea'
// subprojects {
// apply from: rootProject.file('dagger.gradle')
// }
//
// do not use gradle integration of the ide. instead generate and import like so:
//
// ./gradlew clean cleanEclipse cleanIdea eclipse idea
//
// known limitations:
// as output folders include generated classes, you may need to run clean a few times.
// incompatible with android plugin as it applies the java plugin
// unnecessarily applies both eclipse and idea plugins even if you don't use them
// suffers from the normal non-IDE eclipse integration where nested projects don't import properly.
// change your structure to flattened to avoid this.
//
// deprecated by: https://github.com/Netflix/gradle-template/issues/8
//
// original design: cfieber
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
if (!project.hasProperty('daggerVersion')) {
ext {
daggerVersion = "1.2.2"
}
}
configurations {
daggerCompiler {
visible false
}
}
configurations.all {
resolutionStrategy {
eachDependency { DependencyResolveDetails details ->
if (details.requested.group == 'com.squareup.dagger') {
details.useVersion daggerVersion
}
}
}
}
def annotationGeneratedSources = file('.generated/src')
def annotationGeneratedTestSources = file('.generated/test')
task prepareAnnotationGeneratedSourceDirs(overwrite: true) << {
annotationGeneratedSources.mkdirs()
annotationGeneratedTestSources.mkdirs()
sourceSets*.java.srcDirs*.each { it.mkdirs() }
sourceSets*.resources.srcDirs*.each { it.mkdirs() }
}
sourceSets {
main {
java {
compileClasspath += configurations.daggerCompiler
}
}
test {
java {
compileClasspath += configurations.daggerCompiler
}
}
}
dependencies {
compile "com.squareup.dagger:dagger:${project.daggerVersion}"
daggerCompiler "com.squareup.dagger:dagger-compiler:${project.daggerVersion}"
}
rootProject.idea.project.ipr.withXml { projectXml ->
projectXml.asNode().component.find { it.@name == 'CompilerConfiguration' }.annotationProcessing[0].replaceNode {
annotationProcessing {
profile(default: true, name: 'Default', enabled: true) {
sourceOutputDir name: relativePath(annotationGeneratedSources)
sourceTestOutputDir name: relativePath(annotationGeneratedTestSources)
outputRelativeToContentRoot value: true
processorPath useClasspath: true
}
}
}
}
tasks.ideaModule.dependsOn(prepareAnnotationGeneratedSourceDirs)
idea.module {
scopes.PROVIDED.plus += [project.configurations.daggerCompiler]
iml.withXml { xml->
def moduleSource = xml.asNode().component.find { it.@name = 'NewModuleRootManager' }.content[0]
moduleSource.appendNode('sourceFolder', [url: "file://\$MODULE_DIR\$/${relativePath(annotationGeneratedSources)}", isTestSource: false])
moduleSource.appendNode('sourceFolder', [url: "file://\$MODULE_DIR\$/${relativePath(annotationGeneratedTestSources)}", isTestSource: true])
}
}
tasks.eclipseClasspath.dependsOn(prepareAnnotationGeneratedSourceDirs)
eclipse.classpath {
plusConfigurations += [project.configurations.daggerCompiler]
}
tasks.eclipseClasspath {
doLast {
eclipse.classpath.file.withXml {
it.asNode().children()[0] + {
classpathentry(kind: 'src', path: relativePath(annotationGeneratedSources)) {
attributes {
attribute name: 'optional', value: true
}
}
}
}
}
}
// http://forums.gradle.org/gradle/topics/eclipse_generated_files_should_be_put_in_the_same_place_as_the_gradle_generated_files
Map pathMappings = [:];
SourceSetContainer sourceSets = project.sourceSets;
sourceSets.each { SourceSet sourceSet ->
String relativeJavaOutputDirectory = project.relativePath(sourceSet.output.classesDir);
String relativeResourceOutputDirectory = project.relativePath(sourceSet.output.resourcesDir);
sourceSet.java.getSrcDirTrees().each { DirectoryTree sourceDirectory ->
String relativeSrcPath = project.relativePath(sourceDirectory.dir.absolutePath);
pathMappings[relativeSrcPath] = relativeJavaOutputDirectory;
}
sourceSet.resources.getSrcDirTrees().each { DirectoryTree resourceDirectory ->
String relativeResourcePath = project.relativePath(resourceDirectory.dir.absolutePath);
pathMappings[relativeResourcePath] = relativeResourceOutputDirectory;
}
}
project.eclipse.classpath.file {
whenMerged { classpath ->
classpath.entries.findAll { entry ->
return entry.kind == 'src';
}.each { entry ->
if(pathMappings.containsKey(entry.path)) {
entry.output = pathMappings[entry.path];
}
}
}
}
eclipse.jdt.file.withProperties { props ->
props.setProperty('org.eclipse.jdt.core.compiler.processAnnotations', 'enabled')
}
tasks.eclipseJdt {
doFirst {
def aptPrefs = file('.settings/org.eclipse.jdt.apt.core.prefs')
aptPrefs.parentFile.mkdirs()
aptPrefs.text = """\
eclipse.preferences.version=1
org.eclipse.jdt.apt.aptEnabled=true
org.eclipse.jdt.apt.genSrcDir=${relativePath(annotationGeneratedSources)}
org.eclipse.jdt.apt.reconcileEnabled=true
""".stripIndent()
file('.factorypath').withWriter {
new groovy.xml.MarkupBuilder(it).'factorypath' {
project.configurations.daggerCompiler.files.each { dep ->
'factorypathentry' kind: 'EXTJAR', id: dep.absolutePath, enabled: true, runInBatchMode: false
}
}
}
}
}

6
gson/README.md

@ -11,9 +11,3 @@ GitHub github = Feign.builder()
.decoder(new GsonDecoder()) .decoder(new GsonDecoder())
.target(GitHub.class, "https://api.github.com"); .target(GitHub.class, "https://api.github.com");
``` ```
Or add them to your Dagger object graph like so:
```java
GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule());
```

37
gson/src/main/java/feign/gson/GsonCodec.java

@ -1,37 +0,0 @@
package feign.gson;
import com.google.gson.Gson;
import feign.RequestTemplate;
import feign.Response;
import feign.codec.Decoder;
import feign.codec.Encoder;
import javax.inject.Inject;
import java.io.IOException;
import java.lang.reflect.Type;
/**
* @deprecated use {@link GsonEncoder} and {@link GsonDecoder} instead
*/
@Deprecated
public class GsonCodec implements Encoder, Decoder {
private final GsonEncoder encoder;
private final GsonDecoder decoder;
public GsonCodec() {
this(new Gson());
}
@Inject public GsonCodec(Gson gson) {
this.encoder = new GsonEncoder(gson);
this.decoder = new GsonDecoder(gson);
}
@Override public void encode(Object object, RequestTemplate template) {
encoder.encode(object, template);
}
@Override public Object decode(Response response, Type type) throws IOException {
return decoder.decode(response, type);
}
}

9
gson/src/main/java/feign/gson/GsonDecoder.java

@ -17,20 +17,25 @@ package feign.gson;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.JsonIOException; import com.google.gson.JsonIOException;
import com.google.gson.TypeAdapter;
import feign.Response; import feign.Response;
import feign.codec.Decoder; import feign.codec.Decoder;
import java.io.IOException; import java.io.IOException;
import java.io.Reader; import java.io.Reader;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.Collections;
import static feign.Util.ensureClosed; import static feign.Util.ensureClosed;
public class GsonDecoder implements Decoder { public class GsonDecoder implements Decoder {
private final Gson gson; private final Gson gson;
public GsonDecoder(Iterable<TypeAdapter<?>> adapters) {
this(GsonFactory.create(adapters));
}
public GsonDecoder() { public GsonDecoder() {
this(new Gson()); this(Collections.<TypeAdapter<?>>emptyList());
} }
public GsonDecoder(Gson gson) { public GsonDecoder(Gson gson) {

8
gson/src/main/java/feign/gson/GsonEncoder.java

@ -16,14 +16,20 @@
package feign.gson; package feign.gson;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import feign.RequestTemplate; import feign.RequestTemplate;
import feign.codec.Encoder; import feign.codec.Encoder;
import java.util.Collections;
public class GsonEncoder implements Encoder { public class GsonEncoder implements Encoder {
private final Gson gson; private final Gson gson;
public GsonEncoder(Iterable<TypeAdapter<?>> adapters) {
this(GsonFactory.create(adapters));
}
public GsonEncoder() { public GsonEncoder() {
this(new Gson()); this(Collections.<TypeAdapter<?>>emptyList());
} }
public GsonEncoder(Gson gson) { public GsonEncoder(Gson gson) {

46
gson/src/main/java/feign/gson/GsonFactory.java

@ -0,0 +1,46 @@
/*
* Copyright 2013 Netflix, Inc.
*
* 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.gson;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.Map;
import static feign.Util.resolveLastTypeParameter;
final class GsonFactory {
/**
* Registers type adapters by implicit type. Adds one to read numbers in a
* {@code Map<String, Object>} as Integers.
*/
static Gson create(Iterable<TypeAdapter<?>> adapters) {
GsonBuilder builder = new GsonBuilder().setPrettyPrinting();
builder.registerTypeAdapter(new TypeToken<Map<String, Object>>() {
}.getType(), new DoubleToIntMapTypeAdapter());
for (TypeAdapter<?> adapter : adapters) {
Type type = resolveLastTypeParameter(adapter.getClass(), TypeAdapter.class);
builder.registerTypeAdapter(type, adapter);
}
return builder.create();
}
private GsonFactory() {
}
}

92
gson/src/main/java/feign/gson/GsonModule.java

@ -1,92 +0,0 @@
/*
* Copyright 2013 Netflix, Inc.
*
* 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.gson;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;
import dagger.Provides;
import feign.Feign;
import feign.codec.Decoder;
import feign.codec.Encoder;
import javax.inject.Singleton;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.Set;
import static feign.Util.resolveLastTypeParameter;
/**
* <h3>Custom type adapters</h3>
* <br>
* In order to specify custom json parsing,
* {@code Gson} supports {@link TypeAdapter type adapters}. This module adds one
* to read numbers in a {@code Map<String, Object>} as Integers. You can
* customize further by adding additional set bindings to the raw type
* {@code TypeAdapter}.
* <p/>
* <br>
* Here's an example of adding a custom json type adapter.
* <p/>
* <pre>
* &#064;Provides(type = Provides.Type.SET)
* TypeAdapter upperZone() {
* return new TypeAdapter&lt;Zone&gt;() {
*
* &#064;Override
* public void write(JsonWriter out, Zone value) throws IOException {
* throw new IllegalArgumentException();
* }
*
* &#064;Override
* public Zone read(JsonReader in) throws IOException {
* in.beginObject();
* Zone zone = new Zone();
* while (in.hasNext()) {
* zone.put(in.nextName(), in.nextString().toUpperCase());
* }
* in.endObject();
* return zone;
* }
* };
* }
* </pre>
*/
@dagger.Module(injects = Feign.class, addsTo = Feign.Defaults.class)
public final class GsonModule {
@Provides Encoder encoder(Gson gson) {
return new GsonEncoder(gson);
}
@Provides Decoder decoder(Gson gson) {
return new GsonDecoder(gson);
}
@Provides @Singleton Gson gson(Set<TypeAdapter> adapters) {
GsonBuilder builder = new GsonBuilder().setPrettyPrinting();
for (TypeAdapter<?> adapter : adapters) {
Type type = resolveLastTypeParameter(adapter.getClass(), TypeAdapter.class);
builder.registerTypeAdapter(type, adapter);
}
return builder.create();
}
@Provides(type = Provides.Type.SET_VALUES) Set<TypeAdapter> noDefaultTypeAdapters() {
return Collections.emptySet();
}
}

100
gson/src/test/java/feign/gson/GsonModuleTest.java → gson/src/test/java/feign/gson/GsonCodecTest.java

@ -19,13 +19,8 @@ import com.google.gson.TypeAdapter;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter; import com.google.gson.stream.JsonWriter;
import dagger.Module;
import dagger.ObjectGraph;
import dagger.Provides;
import feign.RequestTemplate; import feign.RequestTemplate;
import feign.Response; import feign.Response;
import feign.codec.Decoder;
import feign.codec.Encoder;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
@ -34,7 +29,6 @@ import java.util.LinkedHashMap;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import javax.inject.Inject;
import org.junit.Test; import org.junit.Test;
import static feign.Util.UTF_8; import static feign.Util.UTF_8;
@ -42,35 +36,14 @@ import static feign.assertj.FeignAssertions.assertThat;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
public class GsonModuleTest { public class GsonCodecTest {
@Module(includes = GsonModule.class, injects = EncoderAndDecoderBindings.class)
static class EncoderAndDecoderBindings {
@Inject Encoder encoder;
@Inject Decoder decoder;
}
@Test public void providesEncoderDecoder() throws Exception {
EncoderAndDecoderBindings bindings = new EncoderAndDecoderBindings();
ObjectGraph.create(bindings).inject(bindings);
assertEquals(GsonEncoder.class, bindings.encoder.getClass());
assertEquals(GsonDecoder.class, bindings.decoder.getClass());
}
@Module(includes = GsonModule.class, injects = EncoderBindings.class)
static class EncoderBindings {
@Inject Encoder encoder;
}
@Test public void encodesMapObjectNumericalValuesAsInteger() throws Exception { @Test public void encodesMapObjectNumericalValuesAsInteger() throws Exception {
EncoderBindings bindings = new EncoderBindings();
ObjectGraph.create(bindings).inject(bindings);
Map<String, Object> map = new LinkedHashMap<String, Object>(); Map<String, Object> map = new LinkedHashMap<String, Object>();
map.put("foo", 1); map.put("foo", 1);
RequestTemplate template = new RequestTemplate(); RequestTemplate template = new RequestTemplate();
bindings.encoder.encode(map, template); new GsonEncoder().encode(map, template);
assertThat(template).hasBody("" // assertThat(template).hasBody("" //
+ "{\n" // + "{\n" //
@ -78,17 +51,24 @@ public class GsonModuleTest {
+ "}"); + "}");
} }
@Test public void encodesFormParams() throws Exception { @Test public void decodesMapObjectNumericalValuesAsInteger() throws Exception {
Map<String, Object> map = new LinkedHashMap<String, Object>();
map.put("foo", 1);
EncoderBindings bindings = new EncoderBindings(); Response response =
ObjectGraph.create(bindings).inject(bindings); Response.create(200, "OK", Collections.<String, Collection<String>>emptyMap(), "{\"foo\": 1}", UTF_8);
assertEquals(new GsonDecoder().decode(response, new TypeToken<Map<String, Object>>() {
}.getType()), map);
}
@Test public void encodesFormParams() throws Exception {
Map<String, Object> form = new LinkedHashMap<String, Object>(); Map<String, Object> form = new LinkedHashMap<String, Object>();
form.put("foo", 1); form.put("foo", 1);
form.put("bar", Arrays.asList(2, 3)); form.put("bar", Arrays.asList(2, 3));
RequestTemplate template = new RequestTemplate(); RequestTemplate template = new RequestTemplate();
bindings.encoder.encode(form, template); new GsonEncoder().encode(form, template);
assertThat(template).hasBody("" // assertThat(template).hasBody("" //
+ "{\n" // + "{\n" //
@ -118,14 +98,7 @@ public class GsonModuleTest {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
} }
@Module(includes = GsonModule.class, injects = DecoderBindings.class)
static class DecoderBindings {
@Inject Decoder decoder;
}
@Test public void decodes() throws Exception { @Test public void decodes() throws Exception {
DecoderBindings bindings = new DecoderBindings();
ObjectGraph.create(bindings).inject(bindings);
List<Zone> zones = new LinkedList<Zone>(); List<Zone> zones = new LinkedList<Zone>();
zones.add(new Zone("denominator.io.")); zones.add(new Zone("denominator.io."));
@ -133,16 +106,13 @@ public class GsonModuleTest {
Response response = Response response =
Response.create(200, "OK", Collections.<String, Collection<String>>emptyMap(), zonesJson, UTF_8); Response.create(200, "OK", Collections.<String, Collection<String>>emptyMap(), zonesJson, UTF_8);
assertEquals(zones, bindings.decoder.decode(response, new TypeToken<List<Zone>>() { assertEquals(zones, new GsonDecoder().decode(response, new TypeToken<List<Zone>>() {
}.getType())); }.getType()));
} }
@Test public void nullBodyDecodesToNull() throws Exception { @Test public void nullBodyDecodesToNull() throws Exception {
DecoderBindings bindings = new DecoderBindings();
ObjectGraph.create(bindings).inject(bindings);
Response response = Response.create(204, "OK", Collections.<String, Collection<String>>emptyMap(), (byte[]) null); Response response = Response.create(204, "OK", Collections.<String, Collection<String>>emptyMap(), (byte[]) null);
assertNull(bindings.decoder.decode(response, String.class)); assertNull(new GsonDecoder().decode(response, String.class));
} }
private String zonesJson = ""// private String zonesJson = ""//
@ -156,33 +126,25 @@ public class GsonModuleTest {
+ " }\n"// + " }\n"//
+ "]\n"; + "]\n";
@Module(includes = GsonModule.class, injects = CustomTypeAdapter.class) final TypeAdapter upperZone = new TypeAdapter<Zone>() {
static class CustomTypeAdapter {
@Provides(type = Provides.Type.SET) TypeAdapter upperZone() { @Override public void write(JsonWriter out, Zone value) throws IOException {
return new TypeAdapter<Zone>() { throw new IllegalArgumentException();
@Override public void write(JsonWriter out, Zone value) throws IOException {
throw new IllegalArgumentException();
}
@Override public Zone read(JsonReader in) throws IOException {
in.beginObject();
Zone zone = new Zone();
while (in.hasNext()) {
zone.put(in.nextName(), in.nextString().toUpperCase());
}
in.endObject();
return zone;
}
};
} }
@Inject Decoder decoder; @Override public Zone read(JsonReader in) throws IOException {
} in.beginObject();
Zone zone = new Zone();
while (in.hasNext()) {
zone.put(in.nextName(), in.nextString().toUpperCase());
}
in.endObject();
return zone;
}
};
@Test public void customDecoder() throws Exception { @Test public void customDecoder() throws Exception {
CustomTypeAdapter bindings = new CustomTypeAdapter(); GsonDecoder decoder = new GsonDecoder(Arrays.<TypeAdapter<?>>asList(upperZone));
ObjectGraph.create(bindings).inject(bindings);
List<Zone> zones = new LinkedList<Zone>(); List<Zone> zones = new LinkedList<Zone>();
zones.add(new Zone("DENOMINATOR.IO.")); zones.add(new Zone("DENOMINATOR.IO."));
@ -190,7 +152,7 @@ public class GsonModuleTest {
Response response = Response response =
Response.create(200, "OK", Collections.<String, Collection<String>>emptyMap(), zonesJson, UTF_8); Response.create(200, "OK", Collections.<String, Collection<String>>emptyMap(), zonesJson, UTF_8);
assertEquals(zones, bindings.decoder.decode(response, new TypeToken<List<Zone>>() { assertEquals(zones, decoder.decode(response, new TypeToken<List<Zone>>() {
}.getType())); }.getType()));
} }
} }

7
gson/src/test/java/feign/gson/examples/GitHubExample.java

@ -19,7 +19,6 @@ import feign.Feign;
import feign.Param; import feign.Param;
import feign.RequestLine; import feign.RequestLine;
import feign.gson.GsonDecoder; import feign.gson.GsonDecoder;
import java.util.List; import java.util.List;
/** /**
@ -37,8 +36,10 @@ public class GitHubExample {
int contributions; int contributions;
} }
public static void main(String... args) throws InterruptedException { public static void main(String... args) {
GitHub github = Feign.builder().decoder(new GsonDecoder()).target(GitHub.class, "https://api.github.com"); GitHub github = Feign.builder()
.decoder(new GsonDecoder())
.target(GitHub.class, "https://api.github.com");
System.out.println("Let's fetch and print a list of the contributors to this library."); System.out.println("Let's fetch and print a list of the contributors to this library.");
List<Contributor> contributors = github.contributors("netflix", "feign"); List<Contributor> contributors = github.contributors("netflix", "feign");

6
jackson/README.md

@ -25,9 +25,3 @@ GitHub github = Feign.builder()
.decoder(new JacksonDecoder(mapper)) .decoder(new JacksonDecoder(mapper))
.target(GitHub.class, "https://api.github.com"); .target(GitHub.class, "https://api.github.com");
``` ```
Alternatively, you can add the encoder and decoder to your Dagger object graph using the provided `JacksonModule` like so:
```java
GitHub github = Feign.create(GitHub.class, "https://api.github.com", new JacksonModule());
```

9
jackson/src/main/java/feign/jackson/JacksonDecoder.java

@ -16,6 +16,7 @@
package feign.jackson; package feign.jackson;
import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.RuntimeJsonMappingException; import com.fasterxml.jackson.databind.RuntimeJsonMappingException;
import feign.Response; import feign.Response;
@ -24,12 +25,18 @@ import feign.codec.Decoder;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.Collections;
public class JacksonDecoder implements Decoder { public class JacksonDecoder implements Decoder {
private final ObjectMapper mapper; private final ObjectMapper mapper;
public JacksonDecoder() { public JacksonDecoder() {
this(new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)); this(Collections.<Module>emptyList());
}
public JacksonDecoder(Iterable<Module> modules) {
this(new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerModules(modules));
} }
public JacksonDecoder(ObjectMapper mapper) { public JacksonDecoder(ObjectMapper mapper) {

9
jackson/src/main/java/feign/jackson/JacksonEncoder.java

@ -17,19 +17,26 @@ package feign.jackson;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializationFeature;
import feign.RequestTemplate; import feign.RequestTemplate;
import feign.codec.EncodeException; import feign.codec.EncodeException;
import feign.codec.Encoder; import feign.codec.Encoder;
import java.util.Collections;
public class JacksonEncoder implements Encoder { public class JacksonEncoder implements Encoder {
private final ObjectMapper mapper; private final ObjectMapper mapper;
public JacksonEncoder() { public JacksonEncoder() {
this(Collections.<Module>emptyList());
}
public JacksonEncoder(Iterable<Module> modules) {
this(new ObjectMapper() this(new ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL) .setSerializationInclusion(JsonInclude.Include.NON_NULL)
.configure(SerializationFeature.INDENT_OUTPUT, true)); .configure(SerializationFeature.INDENT_OUTPUT, true)
.registerModules(modules));
} }
public JacksonEncoder(ObjectMapper mapper) { public JacksonEncoder(ObjectMapper mapper) {

103
jackson/src/main/java/feign/jackson/JacksonModule.java

@ -1,103 +0,0 @@
/*
* Copyright 2013 Netflix, Inc.
*
* 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.jackson;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import dagger.Provides;
import feign.Feign;
import feign.codec.Decoder;
import feign.codec.Encoder;
import javax.inject.Singleton;
import java.util.Collections;
import java.util.Set;
/**
* <h3>Custom serializers/deserializers</h3>
* <br>
* In order to specify custom json parsing, Jackson's {@code ObjectMapper} supports {@link JsonSerializer serializers}
* and {@link JsonDeserializer deserializers}, which can be bundled together in {@link Module modules}.
* <p/>
* <br>
* Here's an example of adding a custom module.
* <p/>
* <pre>
* public class ObjectIdSerializer extends StdSerializer&lt;ObjectId&gt; {
* public ObjectIdSerializer() {
* super(ObjectId.class);
* }
*
* &#064;Override
* public void serialize(ObjectId value, JsonGenerator jsonGenerator, SerializerProvider provider) throws IOException {
* jsonGenerator.writeString(value.toString());
* }
* }
*
* public class ObjectIdDeserializer extends StdDeserializer&lt;ObjectId&gt; {
* public ObjectIdDeserializer() {
* super(ObjectId.class);
* }
*
* &#064;Override
* public ObjectId deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException {
* return ObjectId.massageToObjectId(jsonParser.getValueAsString());
* }
* }
*
* public class ObjectIdModule extends SimpleModule {
* public ObjectIdModule() {
* // first deserializers
* addDeserializer(ObjectId.class, new ObjectIdDeserializer());
*
* // then serializers:
* addSerializer(ObjectId.class, new ObjectIdSerializer());
* }
* }
*
* &#064;Provides(type = Provides.Type.SET)
* Module objectIdModule() {
* return new ObjectIdModule();
* }
* </pre>
*/
@dagger.Module(injects = Feign.class, addsTo = Feign.Defaults.class)
public final class JacksonModule {
@Provides Encoder encoder(ObjectMapper mapper) {
return new JacksonEncoder(mapper);
}
@Provides Decoder decoder(ObjectMapper mapper) {
return new JacksonDecoder(mapper);
}
@Provides @Singleton ObjectMapper mapper(Set<Module> modules) {
return new ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.configure(SerializationFeature.INDENT_OUTPUT, true)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerModules(modules);
}
@Provides(type = Provides.Type.SET_VALUES) Set<Module> noDefaultModules() {
return Collections.emptySet();
}
}

70
jackson/src/test/java/feign/jackson/JacksonModuleTest.java → jackson/src/test/java/feign/jackson/JacksonCodecTest.java

@ -4,15 +4,11 @@ import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.module.SimpleModule;
import dagger.Module;
import dagger.ObjectGraph;
import dagger.Provides;
import feign.RequestTemplate; import feign.RequestTemplate;
import feign.Response; import feign.Response;
import feign.codec.Decoder;
import feign.codec.Encoder;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
@ -21,7 +17,6 @@ import java.util.LinkedHashMap;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import javax.inject.Inject;
import org.junit.Test; import org.junit.Test;
import static feign.Util.UTF_8; import static feign.Util.UTF_8;
@ -29,38 +24,14 @@ import static feign.assertj.FeignAssertions.assertThat;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
public class JacksonModuleTest { public class JacksonCodecTest {
@Module(includes = JacksonModule.class, injects = EncoderAndDecoderBindings.class)
static class EncoderAndDecoderBindings {
@Inject
Encoder encoder;
@Inject
Decoder decoder;
}
@Test
public void providesEncoderDecoder() throws Exception {
EncoderAndDecoderBindings bindings = new EncoderAndDecoderBindings();
ObjectGraph.create(bindings).inject(bindings);
assertEquals(JacksonEncoder.class, bindings.encoder.getClass());
assertEquals(JacksonDecoder.class, bindings.decoder.getClass());
}
@Module(includes = JacksonModule.class, injects = EncoderBindings.class)
static class EncoderBindings {
@Inject Encoder encoder;
}
@Test public void encodesMapObjectNumericalValuesAsInteger() throws Exception { @Test public void encodesMapObjectNumericalValuesAsInteger() throws Exception {
EncoderBindings bindings = new EncoderBindings();
ObjectGraph.create(bindings).inject(bindings);
Map<String, Object> map = new LinkedHashMap<String, Object>(); Map<String, Object> map = new LinkedHashMap<String, Object>();
map.put("foo", 1); map.put("foo", 1);
RequestTemplate template = new RequestTemplate(); RequestTemplate template = new RequestTemplate();
bindings.encoder.encode(map, template); new JacksonEncoder().encode(map, template);
assertThat(template).hasBody(""// assertThat(template).hasBody(""//
+ "{\n" // + "{\n" //
@ -69,15 +40,12 @@ public class JacksonModuleTest {
} }
@Test public void encodesFormParams() throws Exception { @Test public void encodesFormParams() throws Exception {
EncoderBindings bindings = new EncoderBindings();
ObjectGraph.create(bindings).inject(bindings);
Map<String, Object> form = new LinkedHashMap<String, Object>(); Map<String, Object> form = new LinkedHashMap<String, Object>();
form.put("foo", 1); form.put("foo", 1);
form.put("bar", Arrays.asList(2, 3)); form.put("bar", Arrays.asList(2, 3));
RequestTemplate template = new RequestTemplate(); RequestTemplate template = new RequestTemplate();
bindings.encoder.encode(form, template); new JacksonEncoder().encode(form, template);
assertThat(template).hasBody(""// assertThat(template).hasBody(""//
+ "{\n" // + "{\n" //
@ -105,31 +73,20 @@ public class JacksonModuleTest {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
} }
@Module(includes = JacksonModule.class, injects = DecoderBindings.class)
static class DecoderBindings {
@Inject Decoder decoder;
}
@Test public void decodes() throws Exception { @Test public void decodes() throws Exception {
DecoderBindings bindings = new DecoderBindings();
ObjectGraph.create(bindings).inject(bindings);
List<Zone> zones = new LinkedList<Zone>(); List<Zone> zones = new LinkedList<Zone>();
zones.add(new Zone("denominator.io.")); zones.add(new Zone("denominator.io."));
zones.add(new Zone("denominator.io.", "ABCD")); zones.add(new Zone("denominator.io.", "ABCD"));
Response response = Response response =
Response.create(200, "OK", Collections.<String, Collection<String>>emptyMap(), zonesJson, UTF_8); Response.create(200, "OK", Collections.<String, Collection<String>>emptyMap(), zonesJson, UTF_8);
assertEquals(zones, bindings.decoder.decode(response, new TypeReference<List<Zone>>() { assertEquals(zones, new JacksonDecoder().decode(response, new TypeReference<List<Zone>>() {
}.getType())); }.getType()));
} }
@Test public void nullBodyDecodesToNull() throws Exception { @Test public void nullBodyDecodesToNull() throws Exception {
DecoderBindings bindings = new DecoderBindings();
ObjectGraph.create(bindings).inject(bindings);
Response response = Response.create(204, "OK", Collections.<String, Collection<String>>emptyMap(), (byte[]) null); Response response = Response.create(204, "OK", Collections.<String, Collection<String>>emptyMap(), (byte[]) null);
assertNull(bindings.decoder.decode(response, String.class)); assertNull(new JacksonDecoder().decode(response, String.class));
} }
private String zonesJson = ""// private String zonesJson = ""//
@ -169,19 +126,8 @@ public class JacksonModuleTest {
} }
} }
@Module(includes = JacksonModule.class, injects = CustomJacksonModule.class)
static class CustomJacksonModule {
@Inject Decoder decoder;
@Provides(type = Provides.Type.SET)
com.fasterxml.jackson.databind.Module upperZone() {
return new ZoneModule();
}
}
@Test public void customDecoder() throws Exception { @Test public void customDecoder() throws Exception {
CustomJacksonModule bindings = new CustomJacksonModule(); JacksonDecoder decoder = new JacksonDecoder(Arrays.<Module>asList(new ZoneModule()));
ObjectGraph.create(bindings).inject(bindings);
List<Zone> zones = new LinkedList<Zone>(); List<Zone> zones = new LinkedList<Zone>();
zones.add(new Zone("DENOMINATOR.IO.")); zones.add(new Zone("DENOMINATOR.IO."));
@ -189,7 +135,7 @@ public class JacksonModuleTest {
Response response = Response response =
Response.create(200, "OK", Collections.<String, Collection<String>>emptyMap(), zonesJson, UTF_8); Response.create(200, "OK", Collections.<String, Collection<String>>emptyMap(), zonesJson, UTF_8);
assertEquals(zones, bindings.decoder.decode(response, new TypeReference<List<Zone>>() { assertEquals(zones, decoder.decode(response, new TypeReference<List<Zone>>() {
}.getType())); }.getType()));
} }
} }

8
jackson/src/test/java/feign/jackson/examples/GitHubExample.java

@ -4,7 +4,6 @@ import feign.Feign;
import feign.Param; import feign.Param;
import feign.RequestLine; import feign.RequestLine;
import feign.jackson.JacksonDecoder; import feign.jackson.JacksonDecoder;
import java.util.List; import java.util.List;
/** /**
@ -29,8 +28,11 @@ public class GitHubExample {
} }
} }
public static void main(String... args) throws InterruptedException { public static void main(String... args) {
GitHub github = Feign.builder().decoder(new JacksonDecoder()).target(GitHub.class, "https://api.github.com"); GitHub github = Feign.builder()
.decoder(new JacksonDecoder())
.target(GitHub.class, "https://api.github.com");
System.out.println("Let's fetch and print a list of the contributors to this library."); System.out.println("Let's fetch and print a list of the contributors to this library.");
List<Contributor> contributors = github.contributors("netflix", "feign"); List<Contributor> contributors = github.contributors("netflix", "feign");
for (Contributor contributor : contributors) { for (Contributor contributor : contributors) {

8
jaxb/README.md

@ -16,11 +16,3 @@ Response response = Feign.builder()
.decoder(new JAXBDecoder(jaxbFactory)) .decoder(new JAXBDecoder(jaxbFactory))
.target(Response.class, "https://apihost"); .target(Response.class, "https://apihost");
``` ```
Alternatively, you can add the encoder and decoder to your Dagger object graph using the provided JAXBModule like so:
```java
JAXBContextFactory jaxbFactory = new JAXBContextFactory.Builder().build();
Response response = Feign.create(Response.class, "https://apihost", new JAXBModule(jaxbFactory));
```

7
jaxb/src/main/java/feign/jaxb/JAXBDecoder.java

@ -19,12 +19,10 @@ import feign.FeignException;
import feign.Response; import feign.Response;
import feign.codec.DecodeException; import feign.codec.DecodeException;
import feign.codec.Decoder; import feign.codec.Decoder;
import javax.inject.Inject;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
/** /**
* Decodes responses using JAXB. * Decodes responses using JAXB.
@ -49,7 +47,6 @@ import java.lang.reflect.Type;
public class JAXBDecoder implements Decoder { public class JAXBDecoder implements Decoder {
private final JAXBContextFactory jaxbContextFactory; private final JAXBContextFactory jaxbContextFactory;
@Inject
public JAXBDecoder(JAXBContextFactory jaxbContextFactory) { public JAXBDecoder(JAXBContextFactory jaxbContextFactory) {
this.jaxbContextFactory = jaxbContextFactory; this.jaxbContextFactory = jaxbContextFactory;
} }

5
jaxb/src/main/java/feign/jaxb/JAXBEncoder.java

@ -18,11 +18,9 @@ package feign.jaxb;
import feign.RequestTemplate; import feign.RequestTemplate;
import feign.codec.EncodeException; import feign.codec.EncodeException;
import feign.codec.Encoder; import feign.codec.Encoder;
import java.io.StringWriter;
import javax.inject.Inject;
import javax.xml.bind.JAXBException; import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller; import javax.xml.bind.Marshaller;
import java.io.StringWriter;
/** /**
* Encodes requests using JAXB. * Encodes requests using JAXB.
@ -47,7 +45,6 @@ import java.io.StringWriter;
public class JAXBEncoder implements Encoder { public class JAXBEncoder implements Encoder {
private final JAXBContextFactory jaxbContextFactory; private final JAXBContextFactory jaxbContextFactory;
@Inject
public JAXBEncoder(JAXBContextFactory jaxbContextFactory) { public JAXBEncoder(JAXBContextFactory jaxbContextFactory) {
this.jaxbContextFactory = jaxbContextFactory; this.jaxbContextFactory = jaxbContextFactory;
} }

66
jaxb/src/main/java/feign/jaxb/JAXBModule.java

@ -1,66 +0,0 @@
/*
* Copyright 2014 Netflix, Inc.
*
* 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.jaxb;
import dagger.Provides;
import feign.Feign;
import feign.codec.Decoder;
import feign.codec.Encoder;
import javax.inject.Singleton;
/**
* Provides an Encoder and Decoder for handling XML responses with JAXB annotated classes.
* <p>
* <br>
* Here is an example of configuring a custom JAXBContextFactory:
* </p>
* <pre>
* JAXBContextFactory jaxbFactory = new JAXBContextFactory.Builder()
* .withMarshallerJAXBEncoding("UTF-8")
* .withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd")
* .build();
*
* Response response = Feign.create(Response.class, "http://apihost", new JAXBModule(jaxbFactory));
* </pre>
* <p>
* The JAXBContextFactory should be reused across requests as it caches the created JAXB contexts.
* </p>
*/
@dagger.Module(injects = Feign.class, addsTo = Feign.Defaults.class)
public final class JAXBModule {
private final JAXBContextFactory jaxbContextFactory;
public JAXBModule() {
this.jaxbContextFactory = new JAXBContextFactory.Builder().build();
}
public JAXBModule(JAXBContextFactory jaxbContextFactory) {
this.jaxbContextFactory = jaxbContextFactory;
}
@Provides Encoder encoder(JAXBEncoder jaxbEncoder) {
return jaxbEncoder;
}
@Provides Decoder decoder(JAXBDecoder jaxbDecoder) {
return jaxbDecoder;
}
@Provides @Singleton JAXBContextFactory jaxbContextFactory() {
return this.jaxbContextFactory;
}
}

57
jaxb/src/test/java/feign/jaxb/JAXBModuleTest.java → jaxb/src/test/java/feign/jaxb/JAXBCodecTest.java

@ -15,15 +15,11 @@
*/ */
package feign.jaxb; package feign.jaxb;
import dagger.Module;
import dagger.ObjectGraph;
import feign.RequestTemplate; import feign.RequestTemplate;
import feign.Response; import feign.Response;
import feign.codec.Decoder;
import feign.codec.Encoder; import feign.codec.Encoder;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import javax.inject.Inject;
import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlElement;
@ -34,34 +30,7 @@ import static feign.Util.UTF_8;
import static feign.assertj.FeignAssertions.assertThat; import static feign.assertj.FeignAssertions.assertThat;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
public class JAXBModuleTest { public class JAXBCodecTest {
@Module(includes = JAXBModule.class, injects = EncoderAndDecoderBindings.class)
static class EncoderAndDecoderBindings {
@Inject
Encoder encoder;
@Inject
Decoder decoder;
}
@Module(includes = JAXBModule.class, injects = EncoderBindings.class)
static class EncoderBindings {
@Inject Encoder encoder;
}
@Module(includes = JAXBModule.class, injects = DecoderBindings.class)
static class DecoderBindings {
@Inject Decoder decoder;
}
@Test
public void providesEncoderDecoder() throws Exception {
EncoderAndDecoderBindings bindings = new EncoderAndDecoderBindings();
ObjectGraph.create(bindings).inject(bindings);
assertEquals(JAXBEncoder.class, bindings.encoder.getClass());
assertEquals(JAXBDecoder.class, bindings.decoder.getClass());
}
@XmlRootElement @XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
@ -87,14 +56,11 @@ public class JAXBModuleTest {
@Test @Test
public void encodesXml() throws Exception { public void encodesXml() throws Exception {
EncoderBindings bindings = new EncoderBindings();
ObjectGraph.create(bindings).inject(bindings);
MockObject mock = new MockObject(); MockObject mock = new MockObject();
mock.value = "Test"; mock.value = "Test";
RequestTemplate template = new RequestTemplate(); RequestTemplate template = new RequestTemplate();
bindings.encoder.encode(mock, template); new JAXBEncoder(new JAXBContextFactory.Builder().build()).encode(mock, template);
assertThat(template).hasBody( assertThat(template).hasBody(
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><mockObject><value>Test</value></mockObject>"); "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><mockObject><value>Test</value></mockObject>");
@ -106,8 +72,7 @@ public class JAXBModuleTest {
.withMarshallerJAXBEncoding("UTF-16") .withMarshallerJAXBEncoding("UTF-16")
.build(); .build();
JAXBModule jaxbModule = new JAXBModule(jaxbContextFactory); Encoder encoder = new JAXBEncoder(jaxbContextFactory);
Encoder encoder = jaxbModule.encoder(new JAXBEncoder(jaxbContextFactory));
MockObject mock = new MockObject(); MockObject mock = new MockObject();
mock.value = "Test"; mock.value = "Test";
@ -125,8 +90,7 @@ public class JAXBModuleTest {
.withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd") .withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd")
.build(); .build();
JAXBModule jaxbModule = new JAXBModule(jaxbContextFactory); Encoder encoder = new JAXBEncoder(jaxbContextFactory);
Encoder encoder = jaxbModule.encoder(new JAXBEncoder(jaxbContextFactory));
MockObject mock = new MockObject(); MockObject mock = new MockObject();
mock.value = "Test"; mock.value = "Test";
@ -146,8 +110,7 @@ public class JAXBModuleTest {
.withMarshallerNoNamespaceSchemaLocation("http://apihost/schema.xsd") .withMarshallerNoNamespaceSchemaLocation("http://apihost/schema.xsd")
.build(); .build();
JAXBModule jaxbModule = new JAXBModule(jaxbContextFactory); Encoder encoder = new JAXBEncoder(jaxbContextFactory);
Encoder encoder = jaxbModule.encoder(new JAXBEncoder(jaxbContextFactory));
MockObject mock = new MockObject(); MockObject mock = new MockObject();
mock.value = "Test"; mock.value = "Test";
@ -167,8 +130,7 @@ public class JAXBModuleTest {
.withMarshallerFormattedOutput(true) .withMarshallerFormattedOutput(true)
.build(); .build();
JAXBModule jaxbModule = new JAXBModule(jaxbContextFactory); Encoder encoder = new JAXBEncoder(jaxbContextFactory);
Encoder encoder = jaxbModule.encoder(new JAXBEncoder(jaxbContextFactory));
MockObject mock = new MockObject(); MockObject mock = new MockObject();
mock.value = "Test"; mock.value = "Test";
@ -187,9 +149,6 @@ public class JAXBModuleTest {
@Test @Test
public void decodesXml() throws Exception { public void decodesXml() throws Exception {
DecoderBindings bindings = new DecoderBindings();
ObjectGraph.create(bindings).inject(bindings);
MockObject mock = new MockObject(); MockObject mock = new MockObject();
mock.value = "Test"; mock.value = "Test";
@ -199,6 +158,8 @@ public class JAXBModuleTest {
Response response = Response response =
Response.create(200, "OK", Collections.<String, Collection<String>>emptyMap(), mockXml, UTF_8); Response.create(200, "OK", Collections.<String, Collection<String>>emptyMap(), mockXml, UTF_8);
assertEquals(mock, bindings.decoder.decode(response, MockObject.class)); JAXBDecoder decoder = new JAXBDecoder(new JAXBContextFactory.Builder().build());
assertEquals(mock, decoder.decode(response, MockObject.class));
} }
} }

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

@ -0,0 +1,123 @@
/*
* Copyright 2013 Netflix, Inc.
*
* 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.jaxrs;
import feign.Contract;
import feign.MethodMetadata;
import javax.ws.rs.Consumes;
import javax.ws.rs.FormParam;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Collection;
import static feign.Util.checkState;
import static feign.Util.emptyToNull;
/**
* Please refer to the
* <a href="https://github.com/Netflix/feign/tree/master/feign-jaxrs">Feign JAX-RS README</a>.
*/
public final class JAXRSContract extends Contract.BaseContract {
static final String ACCEPT = "Accept";
static final String CONTENT_TYPE = "Content-Type";
@Override
public MethodMetadata parseAndValidatateMetadata(Method method) {
MethodMetadata md = super.parseAndValidatateMetadata(method);
Path path = method.getDeclaringClass().getAnnotation(Path.class);
if (path != null) {
String pathValue = emptyToNull(path.value());
checkState(pathValue != null, "Path.value() was empty on type %s", method.getDeclaringClass().getName());
if (!pathValue.startsWith("/")) {
pathValue = "/" + pathValue;
}
md.template().insert(0, pathValue);
}
return md;
}
@Override
protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) {
Class<? extends Annotation> annotationType = methodAnnotation.annotationType();
HttpMethod http = annotationType.getAnnotation(HttpMethod.class);
if (http != null) {
checkState(data.template().method() == null,
"Method %s contains multiple HTTP methods. Found: %s and %s", method.getName(), data.template()
.method(), http.value());
data.template().method(http.value());
} else if (annotationType == Path.class) {
String pathValue = emptyToNull(Path.class.cast(methodAnnotation).value());
checkState(pathValue != null, "Path.value() was empty on method %s", method.getName());
String methodAnnotationValue = Path.class.cast(methodAnnotation).value();
if (!methodAnnotationValue.startsWith("/") && !data.template().toString().endsWith("/")) {
methodAnnotationValue = "/" + methodAnnotationValue;
}
data.template().append(methodAnnotationValue);
} else if (annotationType == Produces.class) {
String[] serverProduces = ((Produces) methodAnnotation).value();
String clientAccepts = serverProduces.length == 0 ? null: emptyToNull(serverProduces[0]);
checkState(clientAccepts != null, "Produces.value() was empty on method %s", method.getName());
data.template().header(ACCEPT, clientAccepts);
} else if (annotationType == Consumes.class) {
String[] serverConsumes = ((Consumes) methodAnnotation).value();
String clientProduces = serverConsumes.length == 0 ? null: emptyToNull(serverConsumes[0]);
checkState(clientProduces != null, "Consumes.value() was empty on method %s", method.getName());
data.template().header(CONTENT_TYPE, clientProduces);
}
}
@Override
protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) {
boolean isHttpParam = false;
for (Annotation parameterAnnotation : annotations) {
Class<? extends Annotation> annotationType = parameterAnnotation.annotationType();
if (annotationType == PathParam.class) {
String name = PathParam.class.cast(parameterAnnotation).value();
checkState(emptyToNull(name) != null, "PathParam.value() was empty on parameter %s", paramIndex);
nameParam(data, name, paramIndex);
isHttpParam = true;
} else if (annotationType == QueryParam.class) {
String name = QueryParam.class.cast(parameterAnnotation).value();
checkState(emptyToNull(name) != null, "QueryParam.value() was empty on parameter %s", paramIndex);
Collection<String> query = addTemplatedParam(data.template().queries().get(name), name);
data.template().query(name, query);
nameParam(data, name, paramIndex);
isHttpParam = true;
} else if (annotationType == HeaderParam.class) {
String name = HeaderParam.class.cast(parameterAnnotation).value();
checkState(emptyToNull(name) != null, "HeaderParam.value() was empty on parameter %s", paramIndex);
Collection<String> header = addTemplatedParam(data.template().headers().get(name), name);
data.template().header(name, header);
nameParam(data, name, paramIndex);
isHttpParam = true;
} else if (annotationType == FormParam.class) {
String name = FormParam.class.cast(parameterAnnotation).value();
checkState(emptyToNull(name) != null, "FormParam.value() was empty on parameter %s", paramIndex);
data.formParams().add(name);
nameParam(data, name, paramIndex);
isHttpParam = true;
}
}
return isHttpParam;
}
}

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

@ -1,133 +0,0 @@
/*
* Copyright 2013 Netflix, Inc.
*
* 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.jaxrs;
import dagger.Provides;
import feign.Body;
import feign.Contract;
import feign.MethodMetadata;
import javax.ws.rs.Consumes;
import javax.ws.rs.FormParam;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Collection;
import static feign.Util.checkState;
import static feign.Util.emptyToNull;
/**
* Please refer to the
* <a href="https://github.com/Netflix/feign/tree/master/feign-jaxrs">Feign JAX-RS README</a>.
*/
@dagger.Module(library = true, overrides = true)
public final class JAXRSModule {
static final String ACCEPT = "Accept";
static final String CONTENT_TYPE = "Content-Type";
@Provides Contract provideContract() {
return new JAXRSContract();
}
public static final class JAXRSContract extends Contract.BaseContract {
@Override
public MethodMetadata parseAndValidatateMetadata(Method method) {
MethodMetadata md = super.parseAndValidatateMetadata(method);
Path path = method.getDeclaringClass().getAnnotation(Path.class);
if (path != null) {
String pathValue = emptyToNull(path.value());
checkState(pathValue != null, "Path.value() was empty on type %s", method.getDeclaringClass().getName());
if (!pathValue.startsWith("/")) {
pathValue = "/" + pathValue;
}
md.template().insert(0, pathValue);
}
return md;
}
@Override
protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) {
Class<? extends Annotation> annotationType = methodAnnotation.annotationType();
HttpMethod http = annotationType.getAnnotation(HttpMethod.class);
if (http != null) {
checkState(data.template().method() == null,
"Method %s contains multiple HTTP methods. Found: %s and %s", method.getName(), data.template()
.method(), http.value());
data.template().method(http.value());
} else if (annotationType == Path.class) {
String pathValue = emptyToNull(Path.class.cast(methodAnnotation).value());
checkState(pathValue != null, "Path.value() was empty on method %s", method.getName());
String methodAnnotationValue = Path.class.cast(methodAnnotation).value();
if (!methodAnnotationValue.startsWith("/") && !data.template().toString().endsWith("/")) {
methodAnnotationValue = "/" + methodAnnotationValue;
}
data.template().append(methodAnnotationValue);
} else if (annotationType == Produces.class) {
String[] serverProduces = ((Produces) methodAnnotation).value();
String clientAccepts = serverProduces.length == 0 ? null: emptyToNull(serverProduces[0]);
checkState(clientAccepts != null, "Produces.value() was empty on method %s", method.getName());
data.template().header(ACCEPT, clientAccepts);
} else if (annotationType == Consumes.class) {
String[] serverConsumes = ((Consumes) methodAnnotation).value();
String clientProduces = serverConsumes.length == 0 ? null: emptyToNull(serverConsumes[0]);
checkState(clientProduces != null, "Consumes.value() was empty on method %s", method.getName());
data.template().header(CONTENT_TYPE, clientProduces);
}
}
@Override
protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) {
boolean isHttpParam = false;
for (Annotation parameterAnnotation : annotations) {
Class<? extends Annotation> annotationType = parameterAnnotation.annotationType();
if (annotationType == PathParam.class) {
String name = PathParam.class.cast(parameterAnnotation).value();
checkState(emptyToNull(name) != null, "PathParam.value() was empty on parameter %s", paramIndex);
nameParam(data, name, paramIndex);
isHttpParam = true;
} else if (annotationType == QueryParam.class) {
String name = QueryParam.class.cast(parameterAnnotation).value();
checkState(emptyToNull(name) != null, "QueryParam.value() was empty on parameter %s", paramIndex);
Collection<String> query = addTemplatedParam(data.template().queries().get(name), name);
data.template().query(name, query);
nameParam(data, name, paramIndex);
isHttpParam = true;
} else if (annotationType == HeaderParam.class) {
String name = HeaderParam.class.cast(parameterAnnotation).value();
checkState(emptyToNull(name) != null, "HeaderParam.value() was empty on parameter %s", paramIndex);
Collection<String> header = addTemplatedParam(data.template().headers().get(name), name);
data.template().header(name, header);
nameParam(data, name, paramIndex);
isHttpParam = true;
} else if (annotationType == FormParam.class) {
String name = FormParam.class.cast(parameterAnnotation).value();
checkState(emptyToNull(name) != null, "FormParam.value() was empty on parameter %s", paramIndex);
data.formParams().add(name);
nameParam(data, name, paramIndex);
isHttpParam = true;
}
}
return isHttpParam;
}
}
}

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

@ -44,14 +44,13 @@ import static java.util.Arrays.asList;
import static org.assertj.core.data.MapEntry.entry; import static org.assertj.core.data.MapEntry.entry;
/** /**
* Tests interfaces defined per {@link feign.jaxrs.JAXRSModule.JAXRSContract} are interpreted into expected {@link feign * Tests interfaces defined per {@link feign.jaxrs.JAXRSContract} are interpreted into expected {@link feign
* .RequestTemplate template} * .RequestTemplate template}
* instances. * instances.
*/ */
public class JAXRSContractTest { public class JAXRSContractTest {
@Rule public final ExpectedException thrown = ExpectedException.none(); @Rule public final ExpectedException thrown = ExpectedException.none();
JAXRSContract contract = new JAXRSContract();
JAXRSModule.JAXRSContract contract = new JAXRSModule.JAXRSContract();
interface Methods { interface Methods {
@POST void post(); @POST void post();

28
jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java

@ -15,17 +15,12 @@
*/ */
package feign.jaxrs.examples; package feign.jaxrs.examples;
import dagger.Module;
import dagger.Provides;
import feign.Feign; import feign.Feign;
import feign.Logger; import feign.jaxrs.JAXRSContract;
import feign.gson.GsonModule; import java.util.List;
import feign.jaxrs.JAXRSModule;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
import java.util.List;
/** /**
* adapted from {@code com.example.retrofit.GitHubClient} * adapted from {@code com.example.retrofit.GitHubClient}
@ -43,7 +38,9 @@ public class GitHubExample {
} }
public static void main(String... args) throws InterruptedException { public static void main(String... args) throws InterruptedException {
GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GitHubModule()); GitHub github = Feign.builder()
.contract(new JAXRSContract())
.target(GitHub.class, "https://api.github.com");
System.out.println("Let's fetch and print a list of the contributors to this library."); System.out.println("Let's fetch and print a list of the contributors to this library.");
List<Contributor> contributors = github.contributors("netflix", "feign"); List<Contributor> contributors = github.contributors("netflix", "feign");
@ -51,19 +48,4 @@ public class GitHubExample {
System.out.println(contributor.login + " (" + contributor.contributions + ")"); System.out.println(contributor.login + " (" + contributor.contributions + ")");
} }
} }
/**
* JAXRSModule tells us to process @GET etc annotations
*/
@Module(overrides = true, library = true, includes = {JAXRSModule.class, GsonModule.class})
static class GitHubModule {
@Provides Logger.Level loggingLevel() {
return Logger.Level.BASIC;
}
@Provides Logger logger() {
return new Logger.ErrorLogger();
}
}
} }

12
ribbon/README.md

@ -4,17 +4,17 @@ This module includes a feign `Target` and `Client` adapter to take advantage of
## Conventions ## Conventions
This integration relies on the Feign `Target.url()` being encoded like `https://myAppProd` where `myAppProd` is the ribbon client or loadbalancer name and `myAppProd.ribbon.listOfServers` configuration is set. This integration relies on the Feign `Target.url()` being encoded like `https://myAppProd` where `myAppProd` is the ribbon client or loadbalancer name and `myAppProd.ribbon.listOfServers` configuration is set.
### RibbonModule ### RibbonClient
Adding `RibbonModule` overrides URL resolution of Feign's client, adding smart routing and resiliency capabilities provided by Ribbon. Adding `RibbonClient` overrides URL resolution of Feign's client, adding smart routing and resiliency capabilities provided by Ribbon.
#### Usage #### Usage
instead of  instead of 
```java ```java
MyService api = Feign.create(MyService.class, "https://myAppProd-1234567890.us-east-1.elb.amazonaws.com"); MyService api = Feign.builder().target(MyService.class, "https://myAppProd-1234567890.us-east-1.elb.amazonaws.com");
``` ```
do do
```java ```java
MyService api = Feign.create(MyService.class, "https://myAppProd", new RibbonModule()); MyService api = Feign.builder().client(new RibbonClient()).target(MyService.class, "https://myAppProd");
``` ```
### LoadBalancingTarget ### LoadBalancingTarget
Using or extending `LoadBalancingTarget` will enable dynamic url discovery via ribbon including incrementing server request counts. Using or extending `LoadBalancingTarget` will enable dynamic url discovery via ribbon including incrementing server request counts.
@ -22,9 +22,9 @@ Using or extending `LoadBalancingTarget` will enable dynamic url discovery via r
#### Usage #### Usage
instead of instead of
```java ```java
MyService api = Feign.create(MyService.class, "https://myAppProd-1234567890.us-east-1.elb.amazonaws.com"); MyService api = Feign.builder().target(MyService.class, "https://myAppProd-1234567890.us-east-1.elb.amazonaws.com");
``` ```
do do
```java ```java
MyService api = Feign.create(LoadBalancingTarget.create(MyService.class, "https://myAppProd")); MyService api = Feign.builder().target(LoadBalancingTarget.create(MyService.class, "https://myAppProd"));
``` ```

2
ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java

@ -34,7 +34,7 @@ import static java.lang.String.format;
* <br> * <br>
* Ex. * Ex.
* <pre> * <pre>
* MyService api = Feign.create(LoadBalancingTarget.create(MyService.class, "http://myAppProd")) * MyService api = Feign.builder().target(LoadBalancingTarget.create(MyService.class, "http://myAppProd"))
* </pre> * </pre>
* Where {@code myAppProd} is the ribbon loadbalancer name and {@code myAppProd.ribbon.listOfServers} configuration * Where {@code myAppProd} is the ribbon loadbalancer name and {@code myAppProd.ribbon.listOfServers} configuration
* is set. * is set.

24
ribbon/src/main/java/feign/ribbon/RibbonClient.java

@ -4,18 +4,11 @@ import com.netflix.client.ClientException;
import com.netflix.client.ClientFactory; import com.netflix.client.ClientFactory;
import com.netflix.client.config.IClientConfig; import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.ILoadBalancer; import com.netflix.loadbalancer.ILoadBalancer;
import java.io.IOException;
import java.net.URI;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
import feign.Client; import feign.Client;
import feign.Request; import feign.Request;
import feign.Response; import feign.Response;
import dagger.Lazy; import java.io.IOException;
import java.net.URI;
/** /**
* RibbonClient can be used in Fiegn builder to activate smart routing and resiliency capabilities provided by Ribbon. * RibbonClient can be used in Fiegn builder to activate smart routing and resiliency capabilities provided by Ribbon.
@ -31,18 +24,7 @@ public class RibbonClient implements Client {
private final Client delegate; private final Client delegate;
public RibbonClient() { public RibbonClient() {
this.delegate = new Client.Default( this.delegate = new Client.Default(null, null);
new Lazy<SSLSocketFactory>() {
public SSLSocketFactory get() {
return (SSLSocketFactory)SSLSocketFactory.getDefault();
}
},
new Lazy<HostnameVerifier>() {
public HostnameVerifier get() {
return HttpsURLConnection.getDefaultHostnameVerifier();
}
}
);
} }
public RibbonClient(Client delegate) { public RibbonClient(Client delegate) {

48
ribbon/src/main/java/feign/ribbon/RibbonModule.java

@ -1,48 +0,0 @@
/*
* Copyright 2013 Netflix, Inc.
*
* 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.ribbon;
import dagger.Provides;
import feign.Client;
import javax.inject.Named;
import javax.inject.Singleton;
/**
* Adding this module will override URL resolution of {@link feign.Client Feign's client},
* adding smart routing and resiliency capabilities provided by Ribbon.
* <br>
* When using this, ensure the {@link feign.Target#url()} is set to as {@code http://clientName}
* or {@code https://clientName}. {@link com.netflix.client.config.IClientConfig#getClientName() clientName}
* will lookup the real url and port of your service dynamically.
* <br>
* Ex.
* <pre>
* MyService api = Feign.create(MyService.class, "http://myAppProd", new RibbonModule());
* </pre>
* Where {@code myAppProd} is the ribbon client name and {@code myAppProd.ribbon.listOfServers} configuration
* is set.
*/
@dagger.Module(overrides = true, library = true, complete = false)
public class RibbonModule {
@Provides @Named("delegate") Client delegate(Client.Default delegate) {
return delegate;
}
@Provides @Singleton Client httpClient(@Named("delegate") Client client) {
return new RibbonClient(client);
}
}

32
ribbon/src/test/java/feign/ribbon/RibbonClientTest.java

@ -18,24 +18,19 @@ package feign.ribbon;
import com.squareup.okhttp.mockwebserver.MockResponse; import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.SocketPolicy; import com.squareup.okhttp.mockwebserver.SocketPolicy;
import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule; import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
import dagger.Provides;
import feign.Feign; import feign.Feign;
import feign.Param; import feign.Param;
import feign.RequestLine; import feign.RequestLine;
import feign.codec.Decoder;
import feign.codec.Encoder;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URL;
import static com.netflix.config.ConfigurationManager.getConfigInstance;
import static org.junit.Assert.assertEquals;
import org.junit.After; import org.junit.After;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.TestName; import org.junit.rules.TestName;
import static com.netflix.config.ConfigurationManager.getConfigInstance;
import static org.junit.Assert.assertEquals;
public class RibbonClientTest { public class RibbonClientTest {
@Rule public final TestName testName = new TestName(); @Rule public final TestName testName = new TestName();
@Rule public final MockWebServerRule server1 = new MockWebServerRule(); @Rule public final MockWebServerRule server1 = new MockWebServerRule();
@ -44,17 +39,6 @@ public class RibbonClientTest {
interface TestInterface { interface TestInterface {
@RequestLine("POST /") void post(); @RequestLine("POST /") void post();
@RequestLine("GET /?a={a}") void getWithQueryParameters(@Param("a") String a); @RequestLine("GET /?a={a}") void getWithQueryParameters(@Param("a") String a);
@dagger.Module(injects = Feign.class, overrides = true, addsTo = Feign.Defaults.class)
static class Module {
@Provides Decoder defaultDecoder() {
return new Decoder.Default();
}
@Provides Encoder defaultEncoder() {
return new Encoder.Default();
}
}
} }
@Test public void loadBalancingDefaultPolicyRoundRobin() throws IOException, InterruptedException { @Test public void loadBalancingDefaultPolicyRoundRobin() throws IOException, InterruptedException {
@ -63,8 +47,7 @@ public class RibbonClientTest {
getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.getUrl("")) + "," + hostAndPort(server2.getUrl(""))); getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.getUrl("")) + "," + hostAndPort(server2.getUrl("")));
TestInterface api = Feign.create(TestInterface.class, "http://" + client(), new TestInterface.Module(), TestInterface api = Feign.builder().client(new RibbonClient()).target(TestInterface.class, "http://" + client());
new RibbonModule());
api.post(); api.post();
api.post(); api.post();
@ -81,9 +64,7 @@ public class RibbonClientTest {
getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.getUrl(""))); getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.getUrl("")));
TestInterface api = Feign.builder().client(new RibbonClient()).target(TestInterface.class, "http://" + client());
TestInterface api = Feign.create(TestInterface.class, "http://" + client(), new TestInterface.Module(),
new RibbonModule());
api.post(); api.post();
@ -107,8 +88,7 @@ public class RibbonClientTest {
getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.getUrl(""))); getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.getUrl("")));
TestInterface api = Feign.create(TestInterface.class, "http://" + client(), new TestInterface.Module(), TestInterface api = Feign.builder().client(new RibbonClient()).target(TestInterface.class, "http://" + client());
new RibbonModule());
api.getWithQueryParameters(queryStringValue); api.getWithQueryParameters(queryStringValue);

65
sax/src/main/java/feign/sax/SAXDecoder.java

@ -18,19 +18,17 @@ package feign.sax;
import feign.Response; import feign.Response;
import feign.codec.DecodeException; import feign.codec.DecodeException;
import feign.codec.Decoder; import feign.codec.Decoder;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;
import javax.inject.Provider;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;
import static feign.Util.checkNotNull; import static feign.Util.checkNotNull;
import static feign.Util.checkState; import static feign.Util.checkState;
@ -50,19 +48,6 @@ import static feign.Util.resolveLastTypeParameter;
* .build()) * .build())
* .target(MyApi.class, "http://api"); * .target(MyApi.class, "http://api");
* </pre> * </pre>
* <p/>
* <h4>Advanced example with Dagger</h4>
* <br>
* <pre>
* &#064;Provides
* Decoder saxDecoder(Provider&lt;ContentHandlerForFoo&gt; foo, //
* Provider&lt;ContentHandlerForBar&gt; bar) {
* return SAXDecoder.builder() //
* .registerContentHandler(Foo.class, foo) //
* .registerContentHandler(Bar.class, bar) //
* .build();
* }
* </pre>
*/ */
public class SAXDecoder implements Decoder { public class SAXDecoder implements Decoder {
@ -70,10 +55,9 @@ public class SAXDecoder implements Decoder {
return new Builder(); return new Builder();
} }
// builder as dagger doesn't support wildcard bindings, map bindings, or set bindings of providers.
public static class Builder { public static class Builder {
private final Map<Type, Provider<? extends ContentHandlerWithResult<?>>> handlerProviders = private final Map<Type, ContentHandlerWithResult.Factory<?>> handlerFactories =
new LinkedHashMap<Type, Provider<? extends ContentHandlerWithResult<?>>>(); new LinkedHashMap<Type, ContentHandlerWithResult.Factory<?>>();
/** /**
* Will call {@link Constructor#newInstance(Object...)} on {@code handlerClass} for each content stream. * Will call {@link Constructor#newInstance(Object...)} on {@code handlerClass} for each content stream.
@ -86,13 +70,13 @@ public class SAXDecoder implements Decoder {
*/ */
public <T extends ContentHandlerWithResult<?>> Builder registerContentHandler(Class<T> handlerClass) { public <T extends ContentHandlerWithResult<?>> Builder registerContentHandler(Class<T> handlerClass) {
Type type = resolveLastTypeParameter(checkNotNull(handlerClass, "handlerClass"), ContentHandlerWithResult.class); Type type = resolveLastTypeParameter(checkNotNull(handlerClass, "handlerClass"), ContentHandlerWithResult.class);
return registerContentHandler(type, new NewInstanceProvider(handlerClass)); return registerContentHandler(type, new NewInstanceContentHandlerWithResultFactory(handlerClass));
} }
private static class NewInstanceProvider<T extends ContentHandlerWithResult<?>> implements Provider<T> { private static class NewInstanceContentHandlerWithResultFactory<T> implements ContentHandlerWithResult.Factory<T> {
private final Constructor<T> ctor; private final Constructor<ContentHandlerWithResult<T>> ctor;
private NewInstanceProvider(Class<T> clazz) { private NewInstanceContentHandlerWithResultFactory(Class<ContentHandlerWithResult<T>> clazz) {
try { try {
this.ctor = clazz.getDeclaredConstructor(); this.ctor = clazz.getDeclaredConstructor();
// allow private or package protected ctors // allow private or package protected ctors
@ -102,7 +86,7 @@ public class SAXDecoder implements Decoder {
} }
} }
@Override public T get() { @Override public ContentHandlerWithResult<T> create() {
try { try {
return ctor.newInstance(); return ctor.newInstance();
} catch (Exception e) { } catch (Exception e) {
@ -112,16 +96,16 @@ public class SAXDecoder implements Decoder {
} }
/** /**
* Will call {@link Provider#get()} on {@code handler} for each content stream. * Will call {@link ContentHandlerWithResult.Factory#create()} on {@code handler} for each content stream.
* The {@code handler} is expected to have a generic parameter of {@code type}. * The {@code handler} is expected to have a generic parameter of {@code type}.
*/ */
public Builder registerContentHandler(Type type, Provider<? extends ContentHandlerWithResult<?>> handler) { public Builder registerContentHandler(Type type, ContentHandlerWithResult.Factory<?> handler) {
this.handlerProviders.put(checkNotNull(type, "type"), checkNotNull(handler, "handler")); this.handlerFactories.put(checkNotNull(type, "type"), checkNotNull(handler, "handler"));
return this; return this;
} }
public SAXDecoder build() { public SAXDecoder build() {
return new SAXDecoder(handlerProviders); return new SAXDecoder(handlerFactories);
} }
} }
@ -129,16 +113,21 @@ public class SAXDecoder implements Decoder {
* Implementations are not intended to be shared across requests. * Implementations are not intended to be shared across requests.
*/ */
public interface ContentHandlerWithResult<T> extends ContentHandler { public interface ContentHandlerWithResult<T> extends ContentHandler {
public interface Factory<T> {
ContentHandlerWithResult<T> create();
}
/** /**
* expected to be set following a call to {@link XMLReader#parse(InputSource)} * expected to be set following a call to {@link XMLReader#parse(InputSource)}
*/ */
T result(); T result();
} }
private final Map<Type, Provider<? extends ContentHandlerWithResult<?>>> handlerProviders; private final Map<Type, ContentHandlerWithResult.Factory<?>> handlerFactories;
private SAXDecoder(Map<Type, Provider<? extends ContentHandlerWithResult<?>>> handlerProviders) { private SAXDecoder(Map<Type, ContentHandlerWithResult.Factory<?>> handlerFactories) {
this.handlerProviders = handlerProviders; this.handlerFactories = handlerFactories;
} }
@Override @Override
@ -146,9 +135,9 @@ public class SAXDecoder implements Decoder {
if (response.body() == null) { if (response.body() == null) {
return null; return null;
} }
Provider<? extends ContentHandlerWithResult<?>> handlerProvider = handlerProviders.get(type); ContentHandlerWithResult.Factory<?> handlerFactory = handlerFactories.get(type);
checkState(handlerProvider != null, "type %s not in configured handlers %s", type, handlerProviders.keySet()); checkState(handlerFactory != null, "type %s not in configured handlers %s", type, handlerFactories.keySet());
ContentHandlerWithResult<?> handler = handlerProvider.get(); ContentHandlerWithResult<?> handler = handlerFactory.create();
try { try {
XMLReader xmlReader = XMLReaderFactory.createXMLReader(); XMLReader xmlReader = XMLReaderFactory.createXMLReader();
xmlReader.setFeature("http://xml.org/sax/features/namespaces", false); xmlReader.setFeature("http://xml.org/sax/features/namespaces", false);

34
sax/src/test/java/feign/sax/SAXDecoderTest.java

@ -15,17 +15,12 @@
*/ */
package feign.sax; package feign.sax;
import dagger.ObjectGraph;
import dagger.Provides;
import feign.Response; import feign.Response;
import feign.codec.Decoder; import feign.codec.Decoder;
import java.io.IOException; import java.io.IOException;
import java.text.ParseException; import java.text.ParseException;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import javax.inject.Inject;
import javax.inject.Provider;
import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
@ -35,26 +30,17 @@ import static feign.Util.UTF_8;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
// unbound wildcards are not currently injectable in dagger.
@SuppressWarnings("rawtypes")
public class SAXDecoderTest { public class SAXDecoderTest {
@Rule public final ExpectedException thrown = ExpectedException.none(); @Rule public final ExpectedException thrown = ExpectedException.none();
@dagger.Module(injects = SAXDecoderTest.class) Decoder decoder = SAXDecoder.builder() //
static class Module { .registerContentHandler(NetworkStatus.class, new SAXDecoder.ContentHandlerWithResult.Factory<NetworkStatus>() {
@Provides Decoder saxDecoder(Provider<NetworkStatusHandler> networkStatus) { @Override public SAXDecoder.ContentHandlerWithResult<NetworkStatus> create() {
return SAXDecoder.builder() // return new NetworkStatusHandler();
.registerContentHandler(NetworkStatus.class, networkStatus) // }
.registerContentHandler(NetworkStatusStringHandler.class) // }) //
.build(); .registerContentHandler(NetworkStatusStringHandler.class) //
} .build();
}
@Inject Decoder decoder;
@Before public void inject() {
ObjectGraph.create(new Module()).inject(this);
}
@Test public void parsesConfiguredTypes() throws ParseException, IOException { @Test public void parsesConfiguredTypes() throws ParseException, IOException {
assertEquals(NetworkStatus.FAILED, decoder.decode(statusFailedResponse(), NetworkStatus.class)); assertEquals(NetworkStatus.FAILED, decoder.decode(statusFailedResponse(), NetworkStatus.class));
@ -87,8 +73,6 @@ public class SAXDecoderTest {
static class NetworkStatusStringHandler extends DefaultHandler implements static class NetworkStatusStringHandler extends DefaultHandler implements
SAXDecoder.ContentHandlerWithResult<String> { SAXDecoder.ContentHandlerWithResult<String> {
@Inject NetworkStatusStringHandler() {
}
private StringBuilder currentText = new StringBuilder(); private StringBuilder currentText = new StringBuilder();
@ -115,8 +99,6 @@ public class SAXDecoderTest {
static class NetworkStatusHandler extends DefaultHandler implements static class NetworkStatusHandler extends DefaultHandler implements
SAXDecoder.ContentHandlerWithResult<NetworkStatus> { SAXDecoder.ContentHandlerWithResult<NetworkStatus> {
@Inject NetworkStatusHandler() {
}
private StringBuilder currentText = new StringBuilder(); private StringBuilder currentText = new StringBuilder();

Loading…
Cancel
Save