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 @@ @@ -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
* 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)`

104
README.md

@ -50,35 +50,14 @@ interface Bank { @@ -50,35 +50,14 @@ interface Bank {
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
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.
```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!
@ -87,7 +66,7 @@ You can find [several examples](https://github.com/Netflix/feign/tree/master/cor @@ -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!
### 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:
@ -100,7 +79,7 @@ GitHub github = Feign.builder() @@ -100,7 +79,7 @@ GitHub github = Feign.builder()
```
### 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:
@ -124,7 +103,7 @@ api = Feign.builder() @@ -124,7 +103,7 @@ api = Feign.builder()
```
### 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:
@ -136,7 +115,7 @@ api = Feign.builder() @@ -136,7 +115,7 @@ api = Feign.builder()
```
### 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:
```java
@ -147,7 +126,7 @@ interface GitHub { @@ -147,7 +126,7 @@ interface GitHub {
```
```java
GitHub github = Feign.builder()
.contract(new JAXRSModule.JAXRSContract())
.contract(new JAXRSContract())
.target(GitHub.class, "https://api.github.com");
```
### OkHttp
@ -162,11 +141,12 @@ GitHub github = Feign.builder() @@ -162,11 +141,12 @@ GitHub github = Feign.builder()
```
### 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`.
```java
MyService api = Feign.create(MyService.class, "https://myAppProd", new RibbonModule());
MyService api = Feign.builder().client(new RibbonClient()).target(MyService.class, "https://myAppProd");
```
### SLF4J
@ -206,27 +186,45 @@ GitHub github = Feign.builder() @@ -206,27 +186,45 @@ GitHub github = Feign.builder()
.target(GitHub.class, "https://api.github.com");
```
### Advanced usage and Dagger
#### 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.
### Advanced usage
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
@Provides(type = SET) RequestInterceptor forwardedForInterceptor() {
return new RequestInterceptor() {
@Override public void apply(RequestTemplate template) {
template.header("X-Forwarded-For", "origin.host.com");
}
};
}
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");
```
@Provides(type = SET) RequestInterceptor userAgentInterceptor() {
return new RequestInterceptor() {
@Override public void apply(RequestTemplate template) {
template.header("User-Agent", "My Cool Client");
}
};
The SLF4JLogger (see above) may also be of interest.
#### 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
@ -237,15 +235,3 @@ for example formatting dates. @@ -237,15 +235,3 @@ for example formatting dates.
```java
@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 { @@ -12,6 +12,5 @@ subprojects {
repositories {
jcenter()
}
apply from: rootProject.file('dagger.gradle')
group = "com.netflix.${githubProjectName}" // TEMPLATE: Set to organization of project
}

3
core/build.gradle

@ -3,9 +3,8 @@ apply plugin: 'java' @@ -3,9 +3,8 @@ apply plugin: 'java'
sourceCompatibility = 1.6
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 'org.assertj:assertj-core:1.7.1'
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; @@ -26,12 +26,10 @@ import java.util.List;
import java.util.Map;
import java.util.zip.GZIPOutputStream;
import javax.inject.Inject;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
import dagger.Lazy;
import feign.Request.Options;
import static feign.Util.CONTENT_ENCODING;
@ -55,10 +53,11 @@ public interface Client { @@ -55,10 +53,11 @@ public interface Client {
Response execute(Request request, Options options) throws IOException;
public static class Default implements Client {
private final Lazy<SSLSocketFactory> sslContextFactory;
private final Lazy<HostnameVerifier> hostnameVerifier;
private final SSLSocketFactory sslContextFactory;
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.hostnameVerifier = hostnameVerifier;
}
@ -72,8 +71,12 @@ public interface Client { @@ -72,8 +71,12 @@ public interface Client {
final HttpURLConnection connection = (HttpURLConnection) new URL(request.url()).openConnection();
if (connection instanceof HttpsURLConnection) {
HttpsURLConnection sslCon = (HttpsURLConnection) connection;
sslCon.setSSLSocketFactory(sslContextFactory.get());
sslCon.setHostnameVerifier(hostnameVerifier.get());
if (sslContextFactory != null) {
sslCon.setSSLSocketFactory(sslContextFactory);
}
if (hostnameVerifier != null) {
sslCon.setHostnameVerifier(hostnameVerifier);
}
}
connection.setConnectTimeout(options.connectTimeoutMillis());
connection.setReadTimeout(options.readTimeoutMillis());

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

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

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

@ -15,25 +15,16 @@ @@ -15,25 +15,16 @@
*/
package feign;
import dagger.ObjectGraph;
import dagger.Provides;
import feign.Logger.NoOpLogger;
import feign.ReflectiveFeign.ParseHandlersByName;
import feign.Request.Options;
import feign.Target.HardCodedTarget;
import feign.codec.Decoder;
import feign.codec.Encoder;
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.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* Feign's purpose is to ease development against http apis that feign
@ -55,80 +46,6 @@ public abstract class Feign { @@ -55,80 +46,6 @@ public abstract class Feign {
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>
* Configuration keys are formatted as unresolved <a href=
@ -160,32 +77,18 @@ public abstract class Feign { @@ -160,32 +77,18 @@ public abstract class Feign {
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 {
private final Set<RequestInterceptor> requestInterceptors = new LinkedHashSet<RequestInterceptor>();
@Inject Logger.Level logLevel;
@Inject Contract contract;
@Inject Client client;
@Inject Retryer retryer;
@Inject Logger logger;
Encoder encoder = new Encoder.Default();
Decoder decoder = new Decoder.Default();
@Inject ErrorDecoder errorDecoder;
@Inject Options options;
@Inject InvocationHandlerFactory invocationHandlerFactory;
Builder() {
ObjectGraph.create(new Defaults()).inject(this);
}
private final List<RequestInterceptor> requestInterceptors = new ArrayList<RequestInterceptor>();
private Logger.Level logLevel = Logger.Level.NONE;
private Contract contract = new Contract.Default();
private Client client = new Client.Default(null, null);
private Retryer retryer = new Retryer.Default();
private Logger logger = new NoOpLogger();
private Encoder encoder = new Encoder.Default();
private Decoder decoder = new Decoder.Default();
private ErrorDecoder errorDecoder = new ErrorDecoder.Default();
private Options options = new Options();
private InvocationHandlerFactory invocationHandlerFactory = new InvocationHandlerFactory.Default();
public Builder logLevel(Logger.Level logLevel) {
this.logLevel = logLevel;
@ -262,51 +165,15 @@ public abstract class Feign { @@ -262,51 +165,15 @@ public abstract class Feign {
}
public <T> T target(Target<T> target) {
return ObjectGraph.create(this).get(Feign.class).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;
return build().newInstance(target);
}
@Provides InvocationHandlerFactory invocationHandlerFactory() {
return invocationHandlerFactory;
public Feign build() {
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; @@ -21,6 +21,7 @@ import java.util.Map;
/** Controls reflective method dispatch. */
public interface InvocationHandlerFactory {
/** Like {@link InvocationHandler#invoke(Object, java.lang.reflect.Method, Object[])}, except for a single method. */
interface MethodHandler {
Object invoke(Object[] argv) throws Throwable;

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

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

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

@ -33,19 +33,7 @@ package feign; @@ -33,19 +33,7 @@ package feign;
* <br>
* <br><b>Configuration</b><br>
* <br>
* {@code RequestInterceptors} are configured via Dagger
* {@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>
* {@code RequestInterceptors} are configured via {@link Feign.Builder#requestInterceptors}.
* <br>
* <br><b>Implementation notes</b><br>
* <br>

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

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

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

@ -19,7 +19,6 @@ import com.google.gson.reflect.TypeToken; @@ -19,7 +19,6 @@ import com.google.gson.reflect.TypeToken;
import java.net.URI;
import java.util.Date;
import java.util.List;
import javax.inject.Named;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
@ -264,70 +263,4 @@ public class DefaultContractTest { @@ -264,70 +263,4 @@ public class DefaultContractTest {
assertThat(md.indexToExpanderClass())
.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; @@ -19,8 +19,6 @@ import com.google.gson.Gson;
import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.SocketPolicy;
import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
import dagger.Module;
import dagger.Provides;
import feign.Target.HardCodedTarget;
import feign.codec.Decoder;
import feign.codec.Encoder;
@ -33,19 +31,15 @@ import java.util.Arrays; @@ -33,19 +31,15 @@ import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import javax.inject.Singleton;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import static dagger.Provides.Type.SET;
import static feign.Util.UTF_8;
import static feign.assertj.MockWebServerAssertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
// unbound wildcards are not currently injectable in dagger.
@SuppressWarnings("rawtypes")
public class FeignTest {
@Rule public final ExpectedException thrown = ExpectedException.none();
@Rule public final MockWebServerRule server = new MockWebServerRule();
@ -78,32 +72,12 @@ public class FeignTest { @@ -78,32 +72,12 @@ public class FeignTest {
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 {
server.enqueue(new MockResponse().setBody("foo"));
TestInterface api =
Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module());
TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort());
api.queryParams("user", Arrays.asList("apple", "pear"));
@ -119,12 +93,10 @@ public class FeignTest { @@ -119,12 +93,10 @@ public class FeignTest {
@RequestLine("POST /") void binaryRequestBody(byte[] contents);
}
@Test
public void postTemplateParamsResolve() throws IOException, InterruptedException {
@Test public void postTemplateParamsResolve() throws IOException, InterruptedException {
server.enqueue(new MockResponse().setBody("foo"));
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(),
new TestInterface.Module());
TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort());
api.login("netflix", "denominator", "password");
@ -132,24 +104,20 @@ public class FeignTest { @@ -132,24 +104,20 @@ public class FeignTest {
.hasBody("{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}");
}
@Test
public void responseCoercesToStringBody() throws IOException, InterruptedException {
@Test public void responseCoercesToStringBody() throws IOException, InterruptedException {
server.enqueue(new MockResponse().setBody("foo"));
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(),
new TestInterface.Module());
TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort());
Response response = api.response();
assertTrue(response.body().isRepeatable());
assertEquals("foo", response.body().toString());
}
@Test
public void postFormParams() throws IOException, InterruptedException {
@Test public void postFormParams() throws IOException, InterruptedException {
server.enqueue(new MockResponse().setBody("foo"));
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(),
new TestInterface.Module());
TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort());
api.form("netflix", "denominator", "password");
@ -157,12 +125,10 @@ public class FeignTest { @@ -157,12 +125,10 @@ public class FeignTest {
.hasBody("{\"customer_name\":\"netflix\",\"user_name\":\"denominator\",\"password\":\"password\"}");
}
@Test
public void postBodyParam() throws IOException, InterruptedException {
@Test public void postBodyParam() throws IOException, InterruptedException {
server.enqueue(new MockResponse().setBody("foo"));
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(),
new TestInterface.Module());
TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort());
api.body(Arrays.asList("netflix", "denominator", "password"));
@ -171,12 +137,10 @@ public class FeignTest { @@ -171,12 +137,10 @@ public class FeignTest {
.hasBody("[netflix, denominator, password]");
}
@Test
public void postGZIPEncodedBodyParam() throws IOException, InterruptedException {
@Test public void postGZIPEncodedBodyParam() throws IOException, InterruptedException {
server.enqueue(new MockResponse().setBody("foo"));
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(),
new TestInterface.Module());
TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort());
api.gzipBody(Arrays.asList("netflix", "denominator", "password"));
@ -185,23 +149,18 @@ public class FeignTest { @@ -185,23 +149,18 @@ public class FeignTest {
.hasGzippedBody("[netflix, denominator, password]".getBytes(UTF_8));
}
@Module(library = true)
static class ForwardedForInterceptor implements RequestInterceptor {
@Provides(type = SET) RequestInterceptor provideThis() {
return this;
}
@Override public void apply(RequestTemplate template) {
template.header("X-Forwarded-For", "origin.host.com");
}
}
@Test
public void singleInterceptor() throws IOException, InterruptedException {
@Test public void singleInterceptor() throws IOException, InterruptedException {
server.enqueue(new MockResponse().setBody("foo"));
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(),
new TestInterface.Module(), new ForwardedForInterceptor());
TestInterface api = new TestInterfaceBuilder()
.requestInterceptor(new ForwardedForInterceptor())
.target("http://localhost:" + server.getPort());
api.post();
@ -209,35 +168,29 @@ public class FeignTest { @@ -209,35 +168,29 @@ public class FeignTest {
.hasHeaders("X-Forwarded-For: origin.host.com");
}
@Module(library = true)
static class UserAgentInterceptor implements RequestInterceptor {
@Provides(type = SET) RequestInterceptor provideThis() {
return this;
}
@Override public void apply(RequestTemplate template) {
template.header("User-Agent", "Feign");
}
}
@Test
public void multipleInterceptor() throws IOException, InterruptedException {
@Test public void multipleInterceptor() throws IOException, InterruptedException {
server.enqueue(new MockResponse().setBody("foo"));
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(),
new TestInterface.Module(), new ForwardedForInterceptor(), new UserAgentInterceptor());
TestInterface api = new TestInterfaceBuilder()
.requestInterceptor(new ForwardedForInterceptor())
.requestInterceptor(new UserAgentInterceptor())
.target("http://localhost:" + server.getPort());
api.post();
assertThat(server.takeRequest())
.hasHeaders("X-Forwarded-For: origin.host.com", "User-Agent: Feign");
assertThat(server.takeRequest()).hasHeaders("X-Forwarded-For: origin.host.com", "User-Agent: Feign");
}
@Test public void customExpander() throws Exception {
server.enqueue(new MockResponse());
TestInterface api =
Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module());
TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort());
api.expand(new Date(1234l));
@ -251,30 +204,21 @@ public class FeignTest { @@ -251,30 +204,21 @@ public class FeignTest {
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 {
@Provides @Singleton ErrorDecoder errorDecoder() {
return new ErrorDecoder.Default() {
@Override
public Exception decode(String methodKey, Response response) {
if (response.status() == 404)
return new IllegalArgumentException("zone not found");
return super.decode(methodKey, response);
}
};
static class IllegalArgumentExceptionOn404 extends ErrorDecoder.Default {
@Override public Exception decode(String methodKey, Response response) {
if (response.status() == 404) return new IllegalArgumentException("zone not found");
return super.decode(methodKey, response);
}
}
@Test
public void canOverrideErrorDecoder() throws IOException, InterruptedException {
@Test public void canOverrideErrorDecoder() throws IOException, InterruptedException {
server.enqueue(new MockResponse().setResponseCode(404).setBody("foo"));
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("zone not found");
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(),
new IllegalArgumentExceptionOn404());
TestInterface api = new TestInterfaceBuilder()
.errorDecoder(new IllegalArgumentExceptionOn404())
.target("http://localhost:" + server.getPort());
api.post();
}
@ -283,83 +227,58 @@ public class FeignTest { @@ -283,83 +227,58 @@ public class FeignTest {
server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START));
server.enqueue(new MockResponse().setBody("success!"));
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(),
new TestInterface.Module());
TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort());
api.post();
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 {
server.enqueue(new MockResponse().setBody("success!"));
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(),
new DecodeFail());
TestInterface api = new TestInterfaceBuilder()
.decoder(new Decoder() {
@Override public Object decode(Response response, Type type) {
return "fail";
}
}).target("http://localhost:" + server.getPort());
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.
*/
public void retryableExceptionInDecoder() throws IOException, InterruptedException {
@Test public void retryableExceptionInDecoder() throws IOException, InterruptedException {
server.enqueue(new MockResponse().setBody("retry!"));
server.enqueue(new MockResponse().setBody("success!"));
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(),
new RetryableExceptionOnRetry());
TestInterface api = new TestInterfaceBuilder()
.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(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
public void doesntRetryAfterResponseIsSent() throws IOException, InterruptedException {
@Test public void doesntRetryAfterResponseIsSent() throws IOException, InterruptedException {
server.enqueue(new MockResponse().setBody("success!"));
thrown.expect(FeignException.class);
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 {
api.post();
@ -424,4 +343,42 @@ public class FeignTest { @@ -424,4 +343,42 @@ public class FeignTest {
assertThat(server.takeRequest())
.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 { @@ -192,7 +192,6 @@ public class LoggerTest {
}
@Test public void unknownHostEmits() throws IOException, InterruptedException {
SendsStuff api = Feign.builder()
.logger(logger)
.logLevel(logLevel)
@ -232,7 +231,6 @@ public class LoggerTest { @@ -232,7 +231,6 @@ public class LoggerTest {
}
@Test public void retryEmits() throws IOException, InterruptedException {
thrown.expect(FeignException.class);
SendsStuff api = Feign.builder()

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

@ -18,7 +18,6 @@ package feign.client; @@ -18,7 +18,6 @@ package feign.client;
import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.SocketPolicy;
import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
import dagger.Lazy;
import feign.Client;
import feign.Feign;
import feign.FeignException;
@ -29,9 +28,7 @@ import java.io.ByteArrayInputStream; @@ -29,9 +28,7 @@ import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.ProtocolException;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
@ -98,15 +95,7 @@ public class DefaultClientTest { @@ -98,15 +95,7 @@ public class DefaultClientTest {
api.patch();
}
Client trustSSLSockets = new Client.Default(new Lazy<SSLSocketFactory>() {
@Override public SSLSocketFactory get() {
return TrustingSSLSocketFactory.get();
}
}, new Lazy<HostnameVerifier>() {
@Override public HostnameVerifier get() {
return HttpsURLConnection.getDefaultHostnameVerifier();
}
});
Client trustSSLSockets = new Client.Default(TrustingSSLSocketFactory.get(), null);
@Test public void canOverrideSSLSocketFactory() throws IOException, InterruptedException {
server.get().useHttps(TrustingSSLSocketFactory.get("localhost"), false);
@ -119,18 +108,9 @@ public class DefaultClientTest { @@ -119,18 +108,9 @@ public class DefaultClientTest {
api.post("foo");
}
Client disableHostnameVerification = new Client.Default(new Lazy<SSLSocketFactory>() {
@Override public SSLSocketFactory get() {
return TrustingSSLSocketFactory.get();
}
}, new Lazy<HostnameVerifier>() {
@Override public HostnameVerifier get() {
return new HostnameVerifier() {
@Override
public boolean verify(String s, SSLSession sslSession) {
return true;
}
};
Client disableHostnameVerification = new Client.Default(TrustingSSLSocketFactory.get(), 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; @@ -36,8 +36,6 @@ import javax.net.ssl.TrustManager;
import javax.net.ssl.X509KeyManager;
import javax.net.ssl.X509TrustManager;
import static com.google.common.base.Throwables.propagate;
/**
* Used for ssl tests to simplify setup.
*/
@ -69,7 +67,7 @@ final class TrustingSSLSocketFactory extends SSLSocketFactory implements X509Tru @@ -69,7 +67,7 @@ final class TrustingSSLSocketFactory extends SSLSocketFactory implements X509Tru
sc.init(new KeyManager[]{this}, new TrustManager[]{this}, new SecureRandom());
this.delegate = sc.getSocketFactory();
} catch (Exception e) {
throw propagate(e);
throw new RuntimeException(e);
}
this.serverAlias = serverAlias;
if (serverAlias.isEmpty()) {
@ -82,7 +80,7 @@ final class TrustingSSLSocketFactory extends SSLSocketFactory implements X509Tru @@ -82,7 +80,7 @@ final class TrustingSSLSocketFactory extends SSLSocketFactory implements X509Tru
Certificate[] rawChain = keyStore.getCertificateChain(serverAlias);
this.certificateChain = Arrays.copyOf(rawChain, rawChain.length, X509Certificate[].class);
} 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 { @@ -48,9 +48,9 @@ public class GitHubExample {
public static void main(String... args) {
GitHub github = Feign.builder()
.decoder(new GsonDecoder())
.logger(new Logger.ErrorLogger())
.logLevel(Logger.Level.BASIC)
.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.");

178
dagger.gradle

@ -1,178 +0,0 @@ @@ -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() @@ -11,9 +11,3 @@ GitHub github = Feign.builder()
.decoder(new GsonDecoder())
.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 @@ @@ -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; @@ -17,20 +17,25 @@ package feign.gson;
import com.google.gson.Gson;
import com.google.gson.JsonIOException;
import com.google.gson.TypeAdapter;
import feign.Response;
import feign.codec.Decoder;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.Type;
import java.util.Collections;
import static feign.Util.ensureClosed;
public class GsonDecoder implements Decoder {
private final Gson gson;
public GsonDecoder(Iterable<TypeAdapter<?>> adapters) {
this(GsonFactory.create(adapters));
}
public GsonDecoder() {
this(new Gson());
this(Collections.<TypeAdapter<?>>emptyList());
}
public GsonDecoder(Gson gson) {

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

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

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

@ -0,0 +1,46 @@ @@ -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 @@ @@ -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; @@ -19,13 +19,8 @@ import com.google.gson.TypeAdapter;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import dagger.Module;
import dagger.ObjectGraph;
import dagger.Provides;
import feign.RequestTemplate;
import feign.Response;
import feign.codec.Decoder;
import feign.codec.Encoder;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
@ -34,7 +29,6 @@ import java.util.LinkedHashMap; @@ -34,7 +29,6 @@ import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import org.junit.Test;
import static feign.Util.UTF_8;
@ -42,35 +36,14 @@ import static feign.assertj.FeignAssertions.assertThat; @@ -42,35 +36,14 @@ import static feign.assertj.FeignAssertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
public class GsonModuleTest {
@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;
}
public class GsonCodecTest {
@Test public void encodesMapObjectNumericalValuesAsInteger() throws Exception {
EncoderBindings bindings = new EncoderBindings();
ObjectGraph.create(bindings).inject(bindings);
Map<String, Object> map = new LinkedHashMap<String, Object>();
map.put("foo", 1);
RequestTemplate template = new RequestTemplate();
bindings.encoder.encode(map, template);
new GsonEncoder().encode(map, template);
assertThat(template).hasBody("" //
+ "{\n" //
@ -78,17 +51,24 @@ public class GsonModuleTest { @@ -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();
ObjectGraph.create(bindings).inject(bindings);
Response response =
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>();
form.put("foo", 1);
form.put("bar", Arrays.asList(2, 3));
RequestTemplate template = new RequestTemplate();
bindings.encoder.encode(form, template);
new GsonEncoder().encode(form, template);
assertThat(template).hasBody("" //
+ "{\n" //
@ -118,14 +98,7 @@ public class GsonModuleTest { @@ -118,14 +98,7 @@ public class GsonModuleTest {
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 {
DecoderBindings bindings = new DecoderBindings();
ObjectGraph.create(bindings).inject(bindings);
List<Zone> zones = new LinkedList<Zone>();
zones.add(new Zone("denominator.io."));
@ -133,16 +106,13 @@ public class GsonModuleTest { @@ -133,16 +106,13 @@ public class GsonModuleTest {
Response response =
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()));
}
@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);
assertNull(bindings.decoder.decode(response, String.class));
assertNull(new GsonDecoder().decode(response, String.class));
}
private String zonesJson = ""//
@ -156,33 +126,25 @@ public class GsonModuleTest { @@ -156,33 +126,25 @@ public class GsonModuleTest {
+ " }\n"//
+ "]\n";
@Module(includes = GsonModule.class, injects = CustomTypeAdapter.class)
static class CustomTypeAdapter {
@Provides(type = Provides.Type.SET) TypeAdapter upperZone() {
return new TypeAdapter<Zone>() {
@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;
}
};
final TypeAdapter upperZone = new TypeAdapter<Zone>() {
@Override public void write(JsonWriter out, Zone value) throws IOException {
throw new IllegalArgumentException();
}
@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 {
CustomTypeAdapter bindings = new CustomTypeAdapter();
ObjectGraph.create(bindings).inject(bindings);
GsonDecoder decoder = new GsonDecoder(Arrays.<TypeAdapter<?>>asList(upperZone));
List<Zone> zones = new LinkedList<Zone>();
zones.add(new Zone("DENOMINATOR.IO."));
@ -190,7 +152,7 @@ public class GsonModuleTest { @@ -190,7 +152,7 @@ public class GsonModuleTest {
Response response =
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()));
}
}

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

@ -19,7 +19,6 @@ import feign.Feign; @@ -19,7 +19,6 @@ import feign.Feign;
import feign.Param;
import feign.RequestLine;
import feign.gson.GsonDecoder;
import java.util.List;
/**
@ -37,8 +36,10 @@ public class GitHubExample { @@ -37,8 +36,10 @@ public class GitHubExample {
int contributions;
}
public static void main(String... args) throws InterruptedException {
GitHub github = Feign.builder().decoder(new GsonDecoder()).target(GitHub.class, "https://api.github.com");
public static void main(String... args) {
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.");
List<Contributor> contributors = github.contributors("netflix", "feign");

6
jackson/README.md

@ -25,9 +25,3 @@ GitHub github = Feign.builder() @@ -25,9 +25,3 @@ GitHub github = Feign.builder()
.decoder(new JacksonDecoder(mapper))
.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 @@ @@ -16,6 +16,7 @@
package feign.jackson;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.RuntimeJsonMappingException;
import feign.Response;
@ -24,12 +25,18 @@ import feign.codec.Decoder; @@ -24,12 +25,18 @@ import feign.codec.Decoder;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.util.Collections;
public class JacksonDecoder implements Decoder {
private final ObjectMapper mapper;
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) {

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

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

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

@ -1,103 +0,0 @@ @@ -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; @@ -4,15 +4,11 @@ import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.type.TypeReference;
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.module.SimpleModule;
import dagger.Module;
import dagger.ObjectGraph;
import dagger.Provides;
import feign.RequestTemplate;
import feign.Response;
import feign.codec.Decoder;
import feign.codec.Encoder;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
@ -21,7 +17,6 @@ import java.util.LinkedHashMap; @@ -21,7 +17,6 @@ import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import org.junit.Test;
import static feign.Util.UTF_8;
@ -29,38 +24,14 @@ import static feign.assertj.FeignAssertions.assertThat; @@ -29,38 +24,14 @@ import static feign.assertj.FeignAssertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
public class JacksonModuleTest {
@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;
}
public class JacksonCodecTest {
@Test public void encodesMapObjectNumericalValuesAsInteger() throws Exception {
EncoderBindings bindings = new EncoderBindings();
ObjectGraph.create(bindings).inject(bindings);
Map<String, Object> map = new LinkedHashMap<String, Object>();
map.put("foo", 1);
RequestTemplate template = new RequestTemplate();
bindings.encoder.encode(map, template);
new JacksonEncoder().encode(map, template);
assertThat(template).hasBody(""//
+ "{\n" //
@ -69,15 +40,12 @@ public class JacksonModuleTest { @@ -69,15 +40,12 @@ public class JacksonModuleTest {
}
@Test public void encodesFormParams() throws Exception {
EncoderBindings bindings = new EncoderBindings();
ObjectGraph.create(bindings).inject(bindings);
Map<String, Object> form = new LinkedHashMap<String, Object>();
form.put("foo", 1);
form.put("bar", Arrays.asList(2, 3));
RequestTemplate template = new RequestTemplate();
bindings.encoder.encode(form, template);
new JacksonEncoder().encode(form, template);
assertThat(template).hasBody(""//
+ "{\n" //
@ -105,31 +73,20 @@ public class JacksonModuleTest { @@ -105,31 +73,20 @@ public class JacksonModuleTest {
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 {
DecoderBindings bindings = new DecoderBindings();
ObjectGraph.create(bindings).inject(bindings);
List<Zone> zones = new LinkedList<Zone>();
zones.add(new Zone("denominator.io."));
zones.add(new Zone("denominator.io.", "ABCD"));
Response response =
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()));
}
@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);
assertNull(bindings.decoder.decode(response, String.class));
assertNull(new JacksonDecoder().decode(response, String.class));
}
private String zonesJson = ""//
@ -169,19 +126,8 @@ public class JacksonModuleTest { @@ -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 {
CustomJacksonModule bindings = new CustomJacksonModule();
ObjectGraph.create(bindings).inject(bindings);
JacksonDecoder decoder = new JacksonDecoder(Arrays.<Module>asList(new ZoneModule()));
List<Zone> zones = new LinkedList<Zone>();
zones.add(new Zone("DENOMINATOR.IO."));
@ -189,7 +135,7 @@ public class JacksonModuleTest { @@ -189,7 +135,7 @@ public class JacksonModuleTest {
Response response =
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()));
}
}

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

@ -4,7 +4,6 @@ import feign.Feign; @@ -4,7 +4,6 @@ import feign.Feign;
import feign.Param;
import feign.RequestLine;
import feign.jackson.JacksonDecoder;
import java.util.List;
/**
@ -29,8 +28,11 @@ public class GitHubExample { @@ -29,8 +28,11 @@ public class GitHubExample {
}
}
public static void main(String... args) throws InterruptedException {
GitHub github = Feign.builder().decoder(new JacksonDecoder()).target(GitHub.class, "https://api.github.com");
public static void main(String... args) {
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.");
List<Contributor> contributors = github.contributors("netflix", "feign");
for (Contributor contributor : contributors) {

8
jaxb/README.md

@ -16,11 +16,3 @@ Response response = Feign.builder() @@ -16,11 +16,3 @@ Response response = Feign.builder()
.decoder(new JAXBDecoder(jaxbFactory))
.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; @@ -19,12 +19,10 @@ import feign.FeignException;
import feign.Response;
import feign.codec.DecodeException;
import feign.codec.Decoder;
import javax.inject.Inject;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import java.io.IOException;
import java.lang.reflect.Type;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
/**
* Decodes responses using JAXB.
@ -49,7 +47,6 @@ import java.lang.reflect.Type; @@ -49,7 +47,6 @@ import java.lang.reflect.Type;
public class JAXBDecoder implements Decoder {
private final JAXBContextFactory jaxbContextFactory;
@Inject
public JAXBDecoder(JAXBContextFactory jaxbContextFactory) {
this.jaxbContextFactory = jaxbContextFactory;
}

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

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

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

@ -1,66 +0,0 @@ @@ -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 @@ @@ -15,15 +15,11 @@
*/
package feign.jaxb;
import dagger.Module;
import dagger.ObjectGraph;
import feign.RequestTemplate;
import feign.Response;
import feign.codec.Decoder;
import feign.codec.Encoder;
import java.util.Collection;
import java.util.Collections;
import javax.inject.Inject;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
@ -34,34 +30,7 @@ import static feign.Util.UTF_8; @@ -34,34 +30,7 @@ import static feign.Util.UTF_8;
import static feign.assertj.FeignAssertions.assertThat;
import static org.junit.Assert.assertEquals;
public class JAXBModuleTest {
@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());
}
public class JAXBCodecTest {
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@ -87,14 +56,11 @@ public class JAXBModuleTest { @@ -87,14 +56,11 @@ public class JAXBModuleTest {
@Test
public void encodesXml() throws Exception {
EncoderBindings bindings = new EncoderBindings();
ObjectGraph.create(bindings).inject(bindings);
MockObject mock = new MockObject();
mock.value = "Test";
RequestTemplate template = new RequestTemplate();
bindings.encoder.encode(mock, template);
new JAXBEncoder(new JAXBContextFactory.Builder().build()).encode(mock, template);
assertThat(template).hasBody(
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><mockObject><value>Test</value></mockObject>");
@ -106,8 +72,7 @@ public class JAXBModuleTest { @@ -106,8 +72,7 @@ public class JAXBModuleTest {
.withMarshallerJAXBEncoding("UTF-16")
.build();
JAXBModule jaxbModule = new JAXBModule(jaxbContextFactory);
Encoder encoder = jaxbModule.encoder(new JAXBEncoder(jaxbContextFactory));
Encoder encoder = new JAXBEncoder(jaxbContextFactory);
MockObject mock = new MockObject();
mock.value = "Test";
@ -125,8 +90,7 @@ public class JAXBModuleTest { @@ -125,8 +90,7 @@ public class JAXBModuleTest {
.withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd")
.build();
JAXBModule jaxbModule = new JAXBModule(jaxbContextFactory);
Encoder encoder = jaxbModule.encoder(new JAXBEncoder(jaxbContextFactory));
Encoder encoder = new JAXBEncoder(jaxbContextFactory);
MockObject mock = new MockObject();
mock.value = "Test";
@ -146,8 +110,7 @@ public class JAXBModuleTest { @@ -146,8 +110,7 @@ public class JAXBModuleTest {
.withMarshallerNoNamespaceSchemaLocation("http://apihost/schema.xsd")
.build();
JAXBModule jaxbModule = new JAXBModule(jaxbContextFactory);
Encoder encoder = jaxbModule.encoder(new JAXBEncoder(jaxbContextFactory));
Encoder encoder = new JAXBEncoder(jaxbContextFactory);
MockObject mock = new MockObject();
mock.value = "Test";
@ -167,8 +130,7 @@ public class JAXBModuleTest { @@ -167,8 +130,7 @@ public class JAXBModuleTest {
.withMarshallerFormattedOutput(true)
.build();
JAXBModule jaxbModule = new JAXBModule(jaxbContextFactory);
Encoder encoder = jaxbModule.encoder(new JAXBEncoder(jaxbContextFactory));
Encoder encoder = new JAXBEncoder(jaxbContextFactory);
MockObject mock = new MockObject();
mock.value = "Test";
@ -187,9 +149,6 @@ public class JAXBModuleTest { @@ -187,9 +149,6 @@ public class JAXBModuleTest {
@Test
public void decodesXml() throws Exception {
DecoderBindings bindings = new DecoderBindings();
ObjectGraph.create(bindings).inject(bindings);
MockObject mock = new MockObject();
mock.value = "Test";
@ -199,6 +158,8 @@ public class JAXBModuleTest { @@ -199,6 +158,8 @@ public class JAXBModuleTest {
Response response =
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 @@ @@ -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 @@ @@ -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; @@ -44,14 +44,13 @@ import static java.util.Arrays.asList;
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}
* instances.
*/
public class JAXRSContractTest {
@Rule public final ExpectedException thrown = ExpectedException.none();
JAXRSModule.JAXRSContract contract = new JAXRSModule.JAXRSContract();
JAXRSContract contract = new JAXRSContract();
interface Methods {
@POST void post();

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

@ -15,17 +15,12 @@ @@ -15,17 +15,12 @@
*/
package feign.jaxrs.examples;
import dagger.Module;
import dagger.Provides;
import feign.Feign;
import feign.Logger;
import feign.gson.GsonModule;
import feign.jaxrs.JAXRSModule;
import feign.jaxrs.JAXRSContract;
import java.util.List;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import java.util.List;
/**
* adapted from {@code com.example.retrofit.GitHubClient}
@ -43,7 +38,9 @@ public class GitHubExample { @@ -43,7 +38,9 @@ public class GitHubExample {
}
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.");
List<Contributor> contributors = github.contributors("netflix", "feign");
@ -51,19 +48,4 @@ public class GitHubExample { @@ -51,19 +48,4 @@ public class GitHubExample {
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 @@ -4,17 +4,17 @@ This module includes a feign `Target` and `Client` adapter to take advantage of
## 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.
### RibbonModule
Adding `RibbonModule` overrides URL resolution of Feign's client, adding smart routing and resiliency capabilities provided by Ribbon.
### RibbonClient
Adding `RibbonClient` overrides URL resolution of Feign's client, adding smart routing and resiliency capabilities provided by Ribbon.
#### Usage
instead of 
```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
```java
MyService api = Feign.create(MyService.class, "https://myAppProd", new RibbonModule());
MyService api = Feign.builder().client(new RibbonClient()).target(MyService.class, "https://myAppProd");
```
### LoadBalancingTarget
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 @@ -22,9 +22,9 @@ Using or extending `LoadBalancingTarget` will enable dynamic url discovery via r
#### Usage
instead of
```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
```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; @@ -34,7 +34,7 @@ import static java.lang.String.format;
* <br>
* Ex.
* <pre>
* MyService api = Feign.create(LoadBalancingTarget.create(MyService.class, "http://myAppProd"))
* MyService api = Feign.builder().target(LoadBalancingTarget.create(MyService.class, "http://myAppProd"))
* </pre>
* Where {@code myAppProd} is the ribbon loadbalancer name and {@code myAppProd.ribbon.listOfServers} configuration
* is set.

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

@ -4,18 +4,11 @@ import com.netflix.client.ClientException; @@ -4,18 +4,11 @@ import com.netflix.client.ClientException;
import com.netflix.client.ClientFactory;
import com.netflix.client.config.IClientConfig;
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.Request;
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.
@ -31,18 +24,7 @@ public class RibbonClient implements Client { @@ -31,18 +24,7 @@ public class RibbonClient implements Client {
private final Client delegate;
public RibbonClient() {
this.delegate = new Client.Default(
new Lazy<SSLSocketFactory>() {
public SSLSocketFactory get() {
return (SSLSocketFactory)SSLSocketFactory.getDefault();
}
},
new Lazy<HostnameVerifier>() {
public HostnameVerifier get() {
return HttpsURLConnection.getDefaultHostnameVerifier();
}
}
);
this.delegate = new Client.Default(null, null);
}
public RibbonClient(Client delegate) {

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

@ -1,48 +0,0 @@ @@ -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; @@ -18,24 +18,19 @@ package feign.ribbon;
import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.SocketPolicy;
import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
import dagger.Provides;
import feign.Feign;
import feign.Param;
import feign.RequestLine;
import feign.codec.Decoder;
import feign.codec.Encoder;
import java.io.IOException;
import java.net.URL;
import static com.netflix.config.ConfigurationManager.getConfigInstance;
import static org.junit.Assert.assertEquals;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import static com.netflix.config.ConfigurationManager.getConfigInstance;
import static org.junit.Assert.assertEquals;
public class RibbonClientTest {
@Rule public final TestName testName = new TestName();
@Rule public final MockWebServerRule server1 = new MockWebServerRule();
@ -44,17 +39,6 @@ public class RibbonClientTest { @@ -44,17 +39,6 @@ public class RibbonClientTest {
interface TestInterface {
@RequestLine("POST /") void post();
@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 {
@ -63,8 +47,7 @@ public class RibbonClientTest { @@ -63,8 +47,7 @@ public class RibbonClientTest {
getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.getUrl("")) + "," + hostAndPort(server2.getUrl("")));
TestInterface api = Feign.create(TestInterface.class, "http://" + client(), new TestInterface.Module(),
new RibbonModule());
TestInterface api = Feign.builder().client(new RibbonClient()).target(TestInterface.class, "http://" + client());
api.post();
api.post();
@ -81,9 +64,7 @@ public class RibbonClientTest { @@ -81,9 +64,7 @@ public class RibbonClientTest {
getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.getUrl("")));
TestInterface api = Feign.create(TestInterface.class, "http://" + client(), new TestInterface.Module(),
new RibbonModule());
TestInterface api = Feign.builder().client(new RibbonClient()).target(TestInterface.class, "http://" + client());
api.post();
@ -107,8 +88,7 @@ public class RibbonClientTest { @@ -107,8 +88,7 @@ public class RibbonClientTest {
getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.getUrl("")));
TestInterface api = Feign.create(TestInterface.class, "http://" + client(), new TestInterface.Module(),
new RibbonModule());
TestInterface api = Feign.builder().client(new RibbonClient()).target(TestInterface.class, "http://" + client());
api.getWithQueryParameters(queryStringValue);

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

@ -18,19 +18,17 @@ package feign.sax; @@ -18,19 +18,17 @@ package feign.sax;
import feign.Response;
import feign.codec.DecodeException;
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.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Type;
import java.util.LinkedHashMap;
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.checkState;
@ -50,19 +48,6 @@ import static feign.Util.resolveLastTypeParameter; @@ -50,19 +48,6 @@ import static feign.Util.resolveLastTypeParameter;
* .build())
* .target(MyApi.class, "http://api");
* </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 {
@ -70,10 +55,9 @@ public class SAXDecoder implements Decoder { @@ -70,10 +55,9 @@ public class SAXDecoder implements Decoder {
return new Builder();
}
// builder as dagger doesn't support wildcard bindings, map bindings, or set bindings of providers.
public static class Builder {
private final Map<Type, Provider<? extends ContentHandlerWithResult<?>>> handlerProviders =
new LinkedHashMap<Type, Provider<? extends ContentHandlerWithResult<?>>>();
private final Map<Type, ContentHandlerWithResult.Factory<?>> handlerFactories =
new LinkedHashMap<Type, ContentHandlerWithResult.Factory<?>>();
/**
* Will call {@link Constructor#newInstance(Object...)} on {@code handlerClass} for each content stream.
@ -86,13 +70,13 @@ public class SAXDecoder implements Decoder { @@ -86,13 +70,13 @@ public class SAXDecoder implements Decoder {
*/
public <T extends ContentHandlerWithResult<?>> Builder registerContentHandler(Class<T> handlerClass) {
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 final Constructor<T> ctor;
private static class NewInstanceContentHandlerWithResultFactory<T> implements ContentHandlerWithResult.Factory<T> {
private final Constructor<ContentHandlerWithResult<T>> ctor;
private NewInstanceProvider(Class<T> clazz) {
private NewInstanceContentHandlerWithResultFactory(Class<ContentHandlerWithResult<T>> clazz) {
try {
this.ctor = clazz.getDeclaredConstructor();
// allow private or package protected ctors
@ -102,7 +86,7 @@ public class SAXDecoder implements Decoder { @@ -102,7 +86,7 @@ public class SAXDecoder implements Decoder {
}
}
@Override public T get() {
@Override public ContentHandlerWithResult<T> create() {
try {
return ctor.newInstance();
} catch (Exception e) {
@ -112,16 +96,16 @@ public class SAXDecoder implements Decoder { @@ -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}.
*/
public Builder registerContentHandler(Type type, Provider<? extends ContentHandlerWithResult<?>> handler) {
this.handlerProviders.put(checkNotNull(type, "type"), checkNotNull(handler, "handler"));
public Builder registerContentHandler(Type type, ContentHandlerWithResult.Factory<?> handler) {
this.handlerFactories.put(checkNotNull(type, "type"), checkNotNull(handler, "handler"));
return this;
}
public SAXDecoder build() {
return new SAXDecoder(handlerProviders);
return new SAXDecoder(handlerFactories);
}
}
@ -129,16 +113,21 @@ public class SAXDecoder implements Decoder { @@ -129,16 +113,21 @@ public class SAXDecoder implements Decoder {
* Implementations are not intended to be shared across requests.
*/
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)}
*/
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) {
this.handlerProviders = handlerProviders;
private SAXDecoder(Map<Type, ContentHandlerWithResult.Factory<?>> handlerFactories) {
this.handlerFactories = handlerFactories;
}
@Override
@ -146,9 +135,9 @@ public class SAXDecoder implements Decoder { @@ -146,9 +135,9 @@ public class SAXDecoder implements Decoder {
if (response.body() == null) {
return null;
}
Provider<? extends ContentHandlerWithResult<?>> handlerProvider = handlerProviders.get(type);
checkState(handlerProvider != null, "type %s not in configured handlers %s", type, handlerProviders.keySet());
ContentHandlerWithResult<?> handler = handlerProvider.get();
ContentHandlerWithResult.Factory<?> handlerFactory = handlerFactories.get(type);
checkState(handlerFactory != null, "type %s not in configured handlers %s", type, handlerFactories.keySet());
ContentHandlerWithResult<?> handler = handlerFactory.create();
try {
XMLReader xmlReader = XMLReaderFactory.createXMLReader();
xmlReader.setFeature("http://xml.org/sax/features/namespaces", false);

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

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

Loading…
Cancel
Save