Compare commits

...

34 Commits
master ... 5.x

Author SHA1 Message Date
Bob T Builder 52792566a3 [Gradle Release Plugin] - new version commit: '5.4.2-SNAPSHOT'. 11 years ago
Bob T Builder a445e7906a [Gradle Release Plugin] - pre tag commit: '5.4.1'. 11 years ago
Adrian Cole 7b40f422ff Merge pull request #89 from allenxwang/5.x 11 years ago
Allen Wang 2ccae7f1be Update to Ribbon 0.3.1 11 years ago
Bob T Builder 4b012249a7 [Gradle Release Plugin] - new version commit: '5.4.1-SNAPSHOT'. 11 years ago
Bob T Builder 3d7a666cb1 [Gradle Release Plugin] - pre tag commit: '5.4.0'. 11 years ago
Adrian Cole a616c9f899 5.4.0-SNAPSHOT 11 years ago
Matt Hurne 2e296883c9 Squashed commit of the following: 11 years ago
David M. Carr 42a74cfaed add support for HTTP basic authentication (#79) 11 years ago
Bob T Builder ec93c04e3e [Gradle Release Plugin] - new version commit: '5.3.1-SNAPSHOT'. 11 years ago
Bob T Builder a6e4b1d91f [Gradle Release Plugin] - pre tag commit: '5.3.0'. 11 years ago
adriancole f501ec7ccc 5.3.0-SNAPSHOT 11 years ago
adriancole 28ff01c66e ribbon 0.2.3 11 years ago
David M. Carr 7f562f6a6a Simplify usage of Gson from Feign.Builder 11 years ago
David M. Carr 206b6e8fe9 Simplify Decoder.Default by extending StringDecoder 11 years ago
Bob T Builder 32ec00e01e [Gradle Release Plugin] - new version commit: '5.2.1-SNAPSHOT'. 11 years ago
Bob T Builder f3bc153e72 [Gradle Release Plugin] - pre tag commit: '5.2.0'. 11 years ago
adriancole 821c71fe5b support usage of gson without using dagger 11 years ago
adriancole 44787e9f95 5.2.0-SNAPSHOT 11 years ago
adriancole 54f1ef3c6a updated docs on decoder 11 years ago
Bob T Builder c8f6bf5f77 [Gradle Release Plugin] - new version commit: '5.1.1-SNAPSHOT'. 11 years ago
Bob T Builder 77835d54f1 [Gradle Release Plugin] - pre tag commit: '5.1.0'. 11 years ago
adriancole 68b54dcf65 Correctly handle IOExceptions wrapped by Ribbon. 11 years ago
adriancole eab2c3194e fixed CHANGES version 11 years ago
adriancole 72ebe88c0a 5.1.0-SNAPSHOT 11 years ago
adriancole b4302301a5 address findbugs 11 years ago
Bob T Builder 99388088b0 [Gradle Release Plugin] - new version commit: '5.0.2-SNAPSHOT'. 11 years ago
Bob T Builder 1f8bcc9bf3 [Gradle Release Plugin] - pre tag commit: '5.0.1'. 11 years ago
adriancole d764e3433a removed dead constants 11 years ago
adriancole 2bb796897b Decoder.decode() is no longer called for Response or void types. 11 years ago
adriancole d105286981 update examples to feign 5.x 11 years ago
Bob T Builder 5c3fd28f43 [Gradle Release Plugin] - new version commit: '5.0.1-SNAPSHOT'. 11 years ago
Bob T Builder dbb8d35fad [Gradle Release Plugin] - pre tag commit: '5.0.0'. 11 years ago
adriancole 99e6747274 removed experimental disclaimer 11 years ago
  1. 19
      CHANGES.md
  2. 76
      README.md
  3. 17
      build.gradle
  4. 2
      core/src/main/java/feign/Client.java
  5. 4
      core/src/main/java/feign/FeignException.java
  6. 5
      core/src/main/java/feign/Logger.java
  7. 16
      core/src/main/java/feign/MethodHandler.java
  8. 8
      core/src/main/java/feign/RetryableException.java
  9. 2
      core/src/main/java/feign/Target.java
  10. 7
      core/src/main/java/feign/Util.java
  11. 69
      core/src/main/java/feign/auth/BasicAuthRequestInterceptor.java
  12. 2
      core/src/main/java/feign/codec/DecodeException.java
  13. 54
      core/src/main/java/feign/codec/Decoder.java
  14. 4
      core/src/main/java/feign/codec/EncodeException.java
  15. 8
      core/src/main/java/feign/codec/StringDecoder.java
  16. 42
      core/src/test/java/feign/FeignTest.java
  17. 5
      core/src/test/java/feign/LoggerTest.java
  18. 2
      core/src/test/java/feign/UtilTest.java
  19. 41
      core/src/test/java/feign/auth/BasicAuthRequestInterceptorTest.java
  20. 16
      core/src/test/java/feign/codec/DefaultDecoderTest.java
  21. 59
      core/src/test/java/feign/examples/GitHubExample.java
  22. 4
      example-github/build.gradle
  23. 47
      example-github/src/main/java/feign/example/github/GitHubExample.java
  24. 4
      example-wikipedia/build.gradle
  25. 15
      example-wikipedia/src/main/java/feign/example/wikipedia/ResponseAdapter.java
  26. 10
      example-wikipedia/src/main/java/feign/example/wikipedia/WikipediaExample.java
  27. 2
      gradle.properties
  28. 13
      gson/README.md
  29. 54
      gson/src/main/java/feign/gson/DoubleToIntMapTypeAdapter.java
  30. 37
      gson/src/main/java/feign/gson/GsonCodec.java
  31. 56
      gson/src/main/java/feign/gson/GsonDecoder.java
  32. 36
      gson/src/main/java/feign/gson/GsonEncoder.java
  33. 88
      gson/src/main/java/feign/gson/GsonModule.java
  34. 12
      gson/src/test/java/feign/gson/GsonModuleTest.java
  35. 4
      gson/src/test/java/feign/gson/examples/GitHubExample.java
  36. 33
      jackson/README.md
  37. 53
      jackson/src/main/java/feign/jackson/JacksonDecoder.java
  38. 46
      jackson/src/main/java/feign/jackson/JacksonEncoder.java
  39. 103
      jackson/src/main/java/feign/jackson/JacksonModule.java
  40. 184
      jackson/src/test/java/feign/jackson/JacksonModuleTest.java
  41. 40
      jackson/src/test/java/feign/jackson/examples/GitHubExample.java
  42. 15
      ribbon/src/main/java/feign/ribbon/LBClient.java
  43. 2
      ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java
  44. 3
      ribbon/src/main/java/feign/ribbon/RibbonModule.java
  45. 5
      ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java
  46. 35
      ribbon/src/test/java/feign/ribbon/RibbonClientTest.java
  47. 2
      sax/src/main/java/feign/sax/SAXDecoder.java
  48. 5
      sax/src/test/java/feign/sax/SAXDecoderTest.java
  49. 8
      sax/src/test/java/feign/sax/examples/AWSSignatureVersion4.java
  50. 2
      settings.gradle

19
CHANGES.md

@ -1,3 +1,22 @@ @@ -1,3 +1,22 @@
### Version 5.4.0
* Add `BasicAuthRequestInterceptor`
* Add Jackson integration
### Version 5.3.0
* Split `GsonCodec` into `GsonEncoder` and `GsonDecoder`, which are easy to use with `Feign.Builder`
* Deprecate `GsonCodec`
* Update to Ribbon 0.2.3
### Version 5.2.0
* Support usage of `GsonCodec` via `Feign.Builder`
### Version 5.1.0
* Correctly handle IOExceptions wrapped by Ribbon.
* Miscellaneous findbugs fixes.
### Version 5.0.1
* `Decoder.decode()` is no longer called for `Response` or `void` types.
### Version 5.0
* Remove support for Observable methods.
* Use single non-generic Decoder/Encoder instead of sets of type-specific Decoders/Encoders.

76
README.md

@ -1,9 +1,6 @@ @@ -1,9 +1,6 @@
# Feign makes writing java http clients easier
Feign is a java to http client binder inspired by [Dagger](https://github.com/square/dagger), [Retrofit](https://github.com/square/retrofit), [JAXRS-2.0](https://jax-rs-spec.java.net/nonav/2.0/apidocs/index.html), and [WebSocket](http://www.oracle.com/technetwork/articles/java/jsr356-1937161.html). Feign's first goal was reducing the complexity of binding [Denominator](https://github.com/Netflix/Denominator) uniformly to http apis regardless of [restfulness](http://www.slideshare.net/adrianfcole/99problems).
## Disclaimer
Feign is experimental and [being simplified further](https://github.com/Netflix/feign/issues/53) in version 5. Particularly, this will impact how encoders and encoders are declared, and remove support for observable methods.
### Why Feign and not X?
You can use tools like Jersey and CXF to write java clients for ReST or SOAP services. You can write your own code on top of http transport libraries like Apache HC. Feign aims to connect your code to http apis with minimal overhead and code. Via customizable decoders and error handling, you should be able to write to any text-based http api.
@ -28,7 +25,9 @@ static class Contributor { @@ -28,7 +25,9 @@ static class Contributor {
}
public static void main(String... args) {
GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule());
GitHub github = Feign.builder()
.decoder(new GsonDecoder())
.target(GitHub.class, "https://api.github.com");
// Fetch and print a list of the contributors to this library.
List<Contributor> contributors = github.contributors("netflix", "feign");
@ -38,8 +37,6 @@ public static void main(String... args) { @@ -38,8 +37,6 @@ public static void main(String... args) {
}
```
Feign includes a fully functional json codec in the `feign-gson` extension. See the `Decoder` section for how to write your own.
### Customization
Feign has several aspects that can be customized. For simple cases, you can use `Feign.builder()` to construct an API interface with your custom components. For example:
@ -59,7 +56,7 @@ For further flexibility, you can use Dagger modules directly. See the `Dagger` @@ -59,7 +56,7 @@ For further flexibility, you can use Dagger modules directly. See the `Dagger`
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");
@ -69,6 +66,12 @@ static class ForwardedForInterceptor implements RequestInterceptor { @@ -69,6 +66,12 @@ static class ForwardedForInterceptor implements RequestInterceptor {
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.
@ -86,9 +89,26 @@ Feign intends to work well within Netflix and other Open Source communities. Mo @@ -86,9 +89,26 @@ Feign intends to work well within Netflix and other Open Source communities. Mo
### Gson
[GsonModule](https://github.com/Netflix/feign/tree/master/gson) adds default encoders and decoders so you get get started with a JSON api.
Integration requires you pass `new GsonModule()` to `Feign.create()`, or add it to your graph with Dagger:
Add `GsonEncoder` and/or `GsonDecoder` to your `Feign.Builder` like so:
```java
GsonCodec codec = new GsonCodec();
GitHub github = Feign.builder()
.encoder(new GsonEncoder())
.decoder(new GsonDecoder())
.target(GitHub.class, "https://api.github.com");
```
### Jackson
[JacksonModule](https://github.com/Netflix/feign/tree/master/jackson) adds an encoder and decoder you can use with a JSON API.
Add `JacksonEncoder` and/or `JacksonDecoder` to your `Feign.Builder` like so:
```java
GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule());
GitHub github = Feign.builder()
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.target(GitHub.class, "https://api.github.com");
```
### Sax
@ -122,26 +142,16 @@ MyService api = Feign.create(MyService.class, "https://myAppProd", new RibbonMod @@ -122,26 +142,16 @@ MyService api = Feign.create(MyService.class, "https://myAppProd", new RibbonMod
```
### Decoders
The last argument to `Feign.create` allows you to specify additional configuration such as how to decode a responses, modeled in Dagger.
`Feign.builder()` allows you to specify additional configuration such as how to decode a response.
If any methods in your interface return types besides `Response`, `void` or `String`, you'll need to configure a `Decoder`.
If any methods in your interface return types besides `Response`, `String` or `void`, you'll need to configure a `Decoder`.
The `GsonModule` in the `feign-gson` extension configures a `Decoder` which parses objects from JSON using reflection.
Here's how to configure json decoding (using the `feign-gson` extension):
Here's how you could write this yourself, using whatever library you prefer:
```java
@Module(library = true)
static class JsonModule {
@Provides Decoder decoder(final JsonParser parser) {
return new Decoder() {
@Override public Object decode(Response response, Type type) throws IOException {
return parser.readJson(response.body().asReader(), type);
}
};
}
}
GitHub github = Feign.builder()
.decoder(new GsonDecoder())
.target(GitHub.class, "https://api.github.com");
```
### Advanced usage and Dagger
@ -169,15 +179,9 @@ Where possible, Feign configuration uses normal Dagger conventions. For example @@ -169,15 +179,9 @@ Where possible, Feign configuration uses normal Dagger conventions. For example
#### 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
@Module(overrides = true)
class Overrides {
@Provides @Singleton Logger.Level provideLoggerLevel() {
return Logger.Level.FULL;
}
@Provides @Singleton Logger provideLogger() {
return new Logger.JavaLogger().appendToFile("logs/http.log");
}
}
GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonGitHubModule(), new Overrides());
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");
```

17
build.gradle

@ -73,6 +73,21 @@ project(':feign-gson') { @@ -73,6 +73,21 @@ project(':feign-gson') {
}
}
project(':feign-jackson') {
apply plugin: 'java'
test {
useTestNG()
}
dependencies {
compile project(':feign-core')
compile 'com.fasterxml.jackson.core:jackson-databind:2.2.2'
testCompile 'org.testng:testng:6.8.5'
testCompile 'com.google.guava:guava:14.0.1'
}
}
project(':feign-jaxrs') {
apply plugin: 'java'
@ -98,7 +113,7 @@ project(':feign-ribbon') { @@ -98,7 +113,7 @@ project(':feign-ribbon') {
dependencies {
compile project(':feign-core')
compile 'com.netflix.ribbon:ribbon-core:0.2.0'
compile 'com.netflix.ribbon:ribbon-core:0.3.1'
testCompile 'org.testng:testng:6.8.5'
testCompile 'com.google.mockwebserver:mockwebserver:20130706'
}

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

@ -144,7 +144,7 @@ public interface Client { @@ -144,7 +144,7 @@ public interface Client {
} else {
stream = connection.getInputStream();
}
Reader body = stream != null ? new InputStreamReader(stream) : null;
Reader body = stream != null ? new InputStreamReader(stream, UTF_8) : null;
return Response.create(status, reason, headers, body, length);
}
}

4
core/src/main/java/feign/FeignException.java

@ -23,8 +23,8 @@ import java.io.IOException; @@ -23,8 +23,8 @@ import java.io.IOException;
* Origin exception type for all Http Apis.
*/
public class FeignException extends RuntimeException {
static FeignException errorReading(Request request, Response response, IOException cause) {
return new FeignException(format("%s %s %s", cause.getMessage(), request.method(), request.url(), 0), cause);
static FeignException errorReading(Request request, Response ignored, IOException cause) {
return new FeignException(format("%s %s %s", cause.getMessage(), request.method(), request.url()), cause);
}
public static FeignException errorStatus(String methodKey, Response response) {

5
core/src/main/java/feign/Logger.java

@ -176,10 +176,9 @@ public abstract class Logger { @@ -176,10 +176,9 @@ public abstract class Logger {
log(configKey, ""); // CRLF
}
Reader body = response.body().asReader();
BufferedReader reader = new BufferedReader(response.body().asReader());
try {
StringBuilder buffered = new StringBuilder();
BufferedReader reader = new BufferedReader(body);
String line;
while ((line = reader.readLine()) != null) {
buffered.append(line);
@ -191,7 +190,7 @@ public abstract class Logger { @@ -191,7 +190,7 @@ public abstract class Logger {
log(configKey, "<--- END HTTP (%s-byte body)", bodyAsString.getBytes(UTF_8).length);
return Response.create(response.status(), response.reason(), response.headers(), bodyAsString);
} finally {
ensureClosed(body);
ensureClosed(reader);
}
}
}

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

@ -65,12 +65,6 @@ interface MethodHandler { @@ -65,12 +65,6 @@ interface MethodHandler {
public RequestTemplate apply(Object[] argv);
}
/**
* same approach as retrofit: temporarily rename threads
*/
static String THREAD_PREFIX = "Feign-";
static String IDLE_THREAD_NAME = THREAD_PREFIX + "Idle";
static final class SynchronousMethodHandler implements MethodHandler {
private final MethodMetadata metadata;
@ -143,7 +137,17 @@ interface MethodHandler { @@ -143,7 +137,17 @@ interface MethodHandler {
response = logger.logAndRebufferResponse(metadata.configKey(), logLevel.get(), response, elapsedTime);
}
if (response.status() >= 200 && response.status() < 300) {
if (Response.class == metadata.returnType()) {
if (response.body() == null) {
return response;
}
String bodyString = Util.toString(response.body().asReader());
return Response.create(response.status(), response.reason(), response.headers(), bodyString);
} else if (void.class == metadata.returnType()) {
return null;
} else {
return decode(response);
}
} else {
throw errorDecoder.decode(metadata.configKey(), response);
}

8
core/src/main/java/feign/RetryableException.java

@ -26,7 +26,7 @@ public class RetryableException extends FeignException { @@ -26,7 +26,7 @@ public class RetryableException extends FeignException {
private static final long serialVersionUID = 1L;
private final Date retryAfter;
private final Long retryAfter;
/**
* @param retryAfter usually corresponds to the {@link feign.Util#RETRY_AFTER}
@ -34,7 +34,7 @@ public class RetryableException extends FeignException { @@ -34,7 +34,7 @@ public class RetryableException extends FeignException {
*/
public RetryableException(String message, Throwable cause, Date retryAfter) {
super(message, cause);
this.retryAfter = retryAfter;
this.retryAfter = retryAfter != null ? retryAfter.getTime() : null;
}
/**
@ -43,7 +43,7 @@ public class RetryableException extends FeignException { @@ -43,7 +43,7 @@ public class RetryableException extends FeignException {
*/
public RetryableException(String message, Date retryAfter) {
super(message);
this.retryAfter = retryAfter;
this.retryAfter = retryAfter != null ? retryAfter.getTime() : null;
}
/**
@ -52,6 +52,6 @@ public class RetryableException extends FeignException { @@ -52,6 +52,6 @@ public class RetryableException extends FeignException {
* application-specific response. Null if unknown.
*/
public Date retryAfter() {
return retryAfter;
return retryAfter != null ? new Date(retryAfter) : null;
}
}

2
core/src/main/java/feign/Target.java

@ -100,6 +100,8 @@ public interface Target<T> { @@ -100,6 +100,8 @@ public interface Target<T> {
}
@Override public boolean equals(Object obj) {
if (obj == null)
return false;
if (this == obj)
return true;
if (HardCodedTarget.class != obj.getClass())

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

@ -60,6 +60,10 @@ public class Util { @@ -60,6 +60,10 @@ public class Util {
* UTF-8: eight-bit UCS Transformation Format.
*/
public static final Charset UTF_8 = Charset.forName("UTF-8");
/**
* ISO-8859-1: ISO Latin Alphabet Number 1 (ISO-LATIN-1).
*/
public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
/**
* Copy of {@code com.google.common.base.Preconditions#checkArgument}.
@ -168,6 +172,9 @@ public class Util { @@ -168,6 +172,9 @@ public class Util {
private static final int BUF_SIZE = 0x800; // 2K chars (4K bytes)
/**
* Adapted from {@code com.google.common.io.CharStreams.toString()}.
*/
public static String toString(Reader reader) throws IOException {
if (reader == null) {
return null;

69
core/src/main/java/feign/auth/BasicAuthRequestInterceptor.java

@ -0,0 +1,69 @@ @@ -0,0 +1,69 @@
/*
* 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.auth;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import sun.misc.BASE64Encoder;
import java.nio.charset.Charset;
import static feign.Util.checkNotNull;
import static feign.Util.ISO_8859_1;
/**
* An interceptor that adds the request header needed to use HTTP basic authentication.
*/
public class BasicAuthRequestInterceptor implements RequestInterceptor {
private final String headerValue;
/**
* Creates an interceptor that authenticates all requests with the specified username and password encoded using
* ISO-8859-1.
*
* @param username the username to use for authentication
* @param password the password to use for authentication
*/
public BasicAuthRequestInterceptor(String username, String password) {
this(username, password, ISO_8859_1);
}
/**
* Creates an interceptor that authenticates all requests with the specified username and password encoded using
* the specified charset.
*
* @param username the username to use for authentication
* @param password the password to use for authentication
* @param charset the charset to use when encoding the credentials
*/
public BasicAuthRequestInterceptor(String username, String password, Charset charset) {
checkNotNull(username, "username");
checkNotNull(password, "password");
this.headerValue = "Basic " + base64Encode((username + ":" + password).getBytes(charset));
}
@Override public void apply(RequestTemplate template) {
template.header("Authorization", headerValue);
}
/*
* This uses a Sun internal method; if we ever encounter a case where this method is not available, the appropriate
* response would be to pull the necessary portions of Guava's BaseEncoding class into Util.
*/
private static String base64Encode(byte[] bytes) {
return new BASE64Encoder().encode(bytes);
}
}

2
core/src/main/java/feign/codec/DecodeException.java

@ -22,7 +22,7 @@ import static feign.Util.checkNotNull; @@ -22,7 +22,7 @@ import static feign.Util.checkNotNull;
/**
* Similar to {@code javax.websocket.DecodeException}, raised when a problem
* occurs decoding a message. Note that {@code DecodeException} is not an
* {@code IOException}, nor have one set as its cause.
* {@code IOException}, nor does it have one set as its cause.
*/
public class DecodeException extends FeignException {

54
core/src/main/java/feign/codec/Decoder.java

@ -17,29 +17,20 @@ package feign.codec; @@ -17,29 +17,20 @@ package feign.codec;
import feign.FeignException;
import feign.Response;
import feign.Util;
import java.io.IOException;
import java.lang.reflect.Type;
import static java.lang.String.format;
/**
* Decodes an HTTP response into a single object of the given {@code Type}. Invoked when
* {@link Response#status()} is in the 2xx range. Like
* {@code javax.websocket.Decoder}, except that the decode method is passed the
* generic type of the target.
*
* <p>
* Decodes an HTTP response into a single object of the given {@code type}. Invoked when
* {@link Response#status()} is in the 2xx range and the return type is neither {@code void} nor {@code Response}.
* <p/>
* <p/>
* Example Implementation:<br>
* <p/>
* <pre>
* public class GsonDecoder implements Decoder {
* private final Gson gson;
*
* public GsonDecoder(Gson gson) {
* this.gson = gson;
* }
* private final Gson gson = new Gson();
*
* &#064;Override
* public Object decode(Response response, Type type) throws IOException {
@ -55,14 +46,25 @@ import static java.lang.String.format; @@ -55,14 +46,25 @@ import static java.lang.String.format;
* }
* }
* </pre>
* <br/>
* <h3>Implementation Note</h3>
* The {@code type} parameter will correspond to the
* {@link java.lang.reflect.Method#getGenericReturnType() generic return type}
* of an {@link feign.Target#type() interface} processed by
* {@link feign.Feign#newInstance(feign.Target)}. When writing your
* implementation of Decoder, ensure you also test parameterized types such as
* {@code List<Foo>}.
*
*/
public interface Decoder {
/**
* Decodes a response into a single object.
* Decodes an http response into an object corresponding to its
* {@link java.lang.reflect.Method#getGenericReturnType() generic return type}.
* If you need to wrap exceptions, please do so via {@link DecodeException}.
*
* @param response the response to decode
* @param type Target object type.
* @param type {@link java.lang.reflect.Method#getGenericReturnType() generic return type}
* of the method corresponding to this {@code response}.
* @return instance of {@code type}
* @throws IOException will be propagated safely to the caller.
* @throws DecodeException when decoding failed due to a checked exception besides IOException.
@ -71,24 +73,8 @@ public interface Decoder { @@ -71,24 +73,8 @@ public interface Decoder {
Object decode(Response response, Type type) throws IOException, DecodeException, FeignException;
/**
* Default implementation of {@code Decoder} that supports {@code void}, {@code Response}, and {@code String}
* signatures.
* Default implementation of {@code Decoder}.
*/
public class Default implements Decoder {
@Override
public Object decode(Response response, Type type) throws IOException {
if (Response.class.equals(type)) {
String bodyString = null;
if (response.body() != null) {
bodyString = Util.toString(response.body().asReader());
}
return Response.create(response.status(), response.reason(), response.headers(), bodyString);
} else if (void.class.equals(type) || response.body() == null) {
return null;
} else if (String.class.equals(type)) {
return Util.toString(response.body().asReader());
}
throw new DecodeException(format("%s is not a type supported by this decoder.", type));
}
public class Default extends StringDecoder {
}
}

4
core/src/main/java/feign/codec/EncodeException.java

@ -21,8 +21,8 @@ import static feign.Util.checkNotNull; @@ -21,8 +21,8 @@ import static feign.Util.checkNotNull;
/**
* Similar to {@code javax.websocket.EncodeException}, raised when a problem
* occurs decoding a message. Note that {@code DecodeException} is not an
* {@code IOException}, nor have one set as its cause.
* occurs encoding a message. Note that {@code EncodeException} is not an
* {@code IOException}, nor does it have one set as its cause.
*/
public class EncodeException extends FeignException {

8
core/src/main/java/feign/codec/StringDecoder.java

@ -21,9 +21,8 @@ import feign.Util; @@ -21,9 +21,8 @@ import feign.Util;
import java.io.IOException;
import java.lang.reflect.Type;
/**
* Adapted from {@code com.google.common.io.CharStreams.toString()}.
*/
import static java.lang.String.format;
public class StringDecoder implements Decoder {
@Override
public Object decode(Response response, Type type) throws IOException {
@ -31,6 +30,9 @@ public class StringDecoder implements Decoder { @@ -31,6 +30,9 @@ public class StringDecoder implements Decoder {
if (body == null) {
return null;
}
if (String.class.equals(type)) {
return Util.toString(body.asReader());
}
throw new DecodeException(format("%s is not a type supported by this decoder.", type));
}
}

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

@ -55,6 +55,8 @@ import static org.testng.Assert.assertTrue; @@ -55,6 +55,8 @@ import static org.testng.Assert.assertTrue;
public class FeignTest {
interface TestInterface {
@RequestLine("POST /") Response response();
@RequestLine("POST /") String post();
@RequestLine("POST /")
@ -134,13 +136,31 @@ public class FeignTest { @@ -134,13 +136,31 @@ public class FeignTest {
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module());
api.login("netflix", "denominator", "password");
assertEquals(new String(server.takeRequest().getBody()),
assertEquals(new String(server.takeRequest().getBody(), UTF_8),
"{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}");
} finally {
server.shutdown();
}
}
@Test
public void responseCoercesToStringBody() throws IOException, InterruptedException {
final MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setBody("foo"));
server.play();
try {
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(),
new TestInterface.Module());
Response response = api.response();
assertTrue(response.body().isRepeatable());
assertEquals(response.body().toString(), "foo");
} finally {
server.shutdown();
}
}
@Test
public void postFormParams() throws IOException, InterruptedException {
final MockWebServer server = new MockWebServer();
@ -151,7 +171,7 @@ public class FeignTest { @@ -151,7 +171,7 @@ public class FeignTest {
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module());
api.form("netflix", "denominator", "password");
assertEquals(new String(server.takeRequest().getBody()),
assertEquals(new String(server.takeRequest().getBody(), UTF_8),
"customer_name=netflix,user_name=denominator,password=password");
} finally {
server.shutdown();
@ -170,7 +190,7 @@ public class FeignTest { @@ -170,7 +190,7 @@ public class FeignTest {
api.body(Arrays.asList("netflix", "denominator", "password"));
RecordedRequest request = server.takeRequest();
assertEquals(request.getHeader("Content-Length"), "32");
assertEquals(new String(request.getBody()), "[netflix, denominator, password]");
assertEquals(new String(request.getBody(), UTF_8), "[netflix, denominator, password]");
} finally {
server.shutdown();
}
@ -297,7 +317,7 @@ public class FeignTest { @@ -297,7 +317,7 @@ public class FeignTest {
@Test public void retriesLostConnectionBeforeRead() throws IOException, InterruptedException {
MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START));
server.enqueue(new MockResponse().setBody("success!".getBytes()));
server.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8)));
server.play();
try {
@ -326,7 +346,7 @@ public class FeignTest { @@ -326,7 +346,7 @@ public class FeignTest {
public void overrideTypeSpecificDecoder() throws IOException, InterruptedException {
MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setBody("success!".getBytes()));
server.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8)));
server.play();
try {
@ -360,8 +380,8 @@ public class FeignTest { @@ -360,8 +380,8 @@ public class FeignTest {
*/
public void retryableExceptionInDecoder() throws IOException, InterruptedException {
MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setBody("retry!".getBytes()));
server.enqueue(new MockResponse().setBody("success!".getBytes()));
server.enqueue(new MockResponse().setBody("retry!".getBytes(UTF_8)));
server.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8)));
server.play();
try {
@ -390,7 +410,7 @@ public class FeignTest { @@ -390,7 +410,7 @@ public class FeignTest {
@Test(expectedExceptions = FeignException.class, expectedExceptionsMessageRegExp = "error reading response POST http://.*")
public void doesntRetryAfterResponseIsSent() throws IOException, InterruptedException {
MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setBody("success!".getBytes()));
server.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8)));
server.play();
try {
@ -414,7 +434,7 @@ public class FeignTest { @@ -414,7 +434,7 @@ public class FeignTest {
@Test public void canOverrideSSLSocketFactory() throws IOException, InterruptedException {
MockWebServer server = new MockWebServer();
server.useHttps(TrustingSSLSocketFactory.get("localhost"), false);
server.enqueue(new MockResponse().setBody("success!".getBytes()));
server.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8)));
server.play();
try {
@ -436,7 +456,7 @@ public class FeignTest { @@ -436,7 +456,7 @@ public class FeignTest {
@Test public void canOverrideHostnameVerifier() throws IOException, InterruptedException {
MockWebServer server = new MockWebServer();
server.useHttps(TrustingSSLSocketFactory.get("bad.example.com"), false);
server.enqueue(new MockResponse().setBody("success!".getBytes()));
server.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8)));
server.play();
try {
@ -452,7 +472,7 @@ public class FeignTest { @@ -452,7 +472,7 @@ public class FeignTest {
MockWebServer server = new MockWebServer();
server.useHttps(TrustingSSLSocketFactory.get("localhost"), false);
server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
server.enqueue(new MockResponse().setBody("success!".getBytes()));
server.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8)));
server.play();
try {

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

@ -33,6 +33,7 @@ import java.util.Arrays; @@ -33,6 +33,7 @@ import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
import static feign.Util.UTF_8;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;
@ -116,7 +117,7 @@ public class LoggerTest { @@ -116,7 +117,7 @@ public class LoggerTest {
assertTrue(messages.get(i).matches(expectedMessages.get(i)), messages.get(i));
}
assertEquals(new String(server.takeRequest().getBody()),
assertEquals(new String(server.takeRequest().getBody(), UTF_8),
"{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}");
} finally {
server.shutdown();
@ -213,7 +214,7 @@ public class LoggerTest { @@ -213,7 +214,7 @@ public class LoggerTest {
assertMessagesMatch(expectedMessages);
assertEquals(new String(server.takeRequest().getBody()),
assertEquals(new String(server.takeRequest().getBody(), UTF_8),
"{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}");
} finally {
server.shutdown();

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

@ -42,7 +42,7 @@ public class UtilTest { @@ -42,7 +42,7 @@ public class UtilTest {
interface Parameterized<T> {
}
class ParameterizedSubtype implements Parameterized<String> {
static class ParameterizedSubtype implements Parameterized<String> {
}
@Test public void resolveLastTypeParameterWhenNotSubtype() throws Exception {

41
core/src/test/java/feign/auth/BasicAuthRequestInterceptorTest.java

@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
/*
* 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.auth;
import feign.RequestTemplate;
import org.testng.annotations.Test;
import java.util.Collection;
import java.util.Collections;
import static org.testng.Assert.assertEquals;
/**
* Tests for {@link BasicAuthRequestInterceptor}.
*/
public class BasicAuthRequestInterceptorTest {
/**
* Tests that request headers are added as expected.
*/
@Test public void testAuthentication() {
RequestTemplate template = new RequestTemplate();
BasicAuthRequestInterceptor interceptor = new BasicAuthRequestInterceptor("Aladdin", "open sesame");
interceptor.apply(template);
Collection<String> actualValue = template.headers().get("Authorization");
Collection<String> expectedValue = Collections.singletonList("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==");
assertEquals(actualValue, expectedValue);
}
}

16
core/src/test/java/feign/codec/DefaultDecoderTest.java

@ -16,7 +16,6 @@ @@ -16,7 +16,6 @@
package feign.codec;
import feign.Response;
import feign.Util;
import org.testng.annotations.Test;
import org.w3c.dom.Document;
@ -32,21 +31,6 @@ import static org.testng.Assert.assertNull; @@ -32,21 +31,6 @@ import static org.testng.Assert.assertNull;
public class DefaultDecoderTest {
private final Decoder decoder = new Decoder.Default();
@Test public void testDecodesToVoid() throws Exception {
assertEquals(decoder.decode(knownResponse(), void.class), null);
}
@Test public void testDecodesToResponse() throws Exception {
Response response = knownResponse();
Object decodedObject = decoder.decode(response, Response.class);
assertEquals(decodedObject.getClass(), Response.class);
Response decodedResponse = (Response) decodedObject;
assertEquals(decodedResponse.status(), response.status());
assertEquals(decodedResponse.reason(), response.reason());
assertEquals(decodedResponse.headers(), response.headers());
assertEquals(Util.toString(decodedResponse.body().asReader()), "response body");
}
@Test public void testDecodesToString() throws Exception {
Response response = knownResponse();
Object decodedObject = decoder.decode(response, String.class);

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

@ -17,18 +17,13 @@ package feign.examples; @@ -17,18 +17,13 @@ package feign.examples;
import com.google.gson.Gson;
import com.google.gson.JsonIOException;
import com.google.gson.stream.JsonReader;
import dagger.Module;
import dagger.Provides;
import feign.Feign;
import feign.Logger;
import feign.RequestLine;
import feign.Response;
import feign.codec.Decoder;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.Type;
@ -51,8 +46,12 @@ public class GitHubExample { @@ -51,8 +46,12 @@ public class GitHubExample {
int contributions;
}
public static void main(String... args) throws InterruptedException {
GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GitHubModule());
public static void main(String... args) {
GitHub github = Feign.builder()
.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.");
List<Contributor> contributors = github.contributors("netflix", "feign");
@ -61,60 +60,26 @@ public class GitHubExample { @@ -61,60 +60,26 @@ public class GitHubExample {
}
}
@Module(overrides = true, library = true, includes = GsonModule.class)
static class GitHubModule {
@Provides Logger.Level loggingLevel() {
return Logger.Level.BASIC;
}
@Provides Logger logger() {
return new Logger.ErrorLogger();
}
}
/**
* Here's how it looks to wire json codecs. Note, that you can always instead use {@code feign-gson}!
* Here's how it looks to write a decoder. Note: you can instead use {@code feign-gson}!
*/
@Module(library = true)
static class GsonModule {
@Provides @Singleton Gson gson() {
return new Gson();
}
@Provides Decoder decoder(GsonDecoder gsonDecoder) {
return gsonDecoder;
}
}
static class GsonDecoder implements Decoder {
private final Gson gson;
@Inject GsonDecoder(Gson gson) {
this.gson = gson;
}
private final Gson gson = new Gson();
@Override public Object decode(Response response, Type type) throws IOException {
if (response.body() == null) {
if (void.class == type || response.body() == null) {
return null;
}
Reader reader = response.body().asReader();
try {
return fromJson(new JsonReader(reader), type);
} finally {
ensureClosed(reader);
}
}
private Object fromJson(JsonReader jsonReader, Type type) throws IOException {
try {
return gson.fromJson(jsonReader, type);
return gson.fromJson(reader, type);
} catch (JsonIOException e) {
if (e.getCause() != null && e.getCause() instanceof IOException) {
throw IOException.class.cast(e.getCause());
}
throw e;
} finally {
ensureClosed(reader);
}
}
}

4
example-github/build.gradle

@ -1,8 +1,8 @@ @@ -1,8 +1,8 @@
apply plugin: 'java'
dependencies {
compile 'com.netflix.feign:feign-core:4.3.0'
compile 'com.netflix.feign:feign-gson:4.3.0'
compile 'com.netflix.feign:feign-core:5.0.0'
compile 'com.netflix.feign:feign-gson:5.0.0'
provided 'com.squareup.dagger:dagger-compiler:1.1.0'
}

47
example-github/src/main/java/feign/example/github/GitHubExample.java

@ -19,14 +19,11 @@ import dagger.Module; @@ -19,14 +19,11 @@ import dagger.Module;
import dagger.Provides;
import feign.Feign;
import feign.Logger;
import feign.Observable;
import feign.Observer;
import feign.RequestLine;
import feign.gson.GsonModule;
import javax.inject.Named;
import java.util.List;
import java.util.concurrent.CountDownLatch;
/**
* adapted from {@code com.example.retrofit.GitHubClient}
@ -36,9 +33,6 @@ public class GitHubExample { @@ -36,9 +33,6 @@ public class GitHubExample {
interface GitHub {
@RequestLine("GET /repos/{owner}/{repo}/contributors")
List<Contributor> contributors(@Named("owner") String owner, @Named("repo") String repo);
@RequestLine("GET /repos/{owner}/{repo}/contributors")
Observable<Contributor> observable(@Named("owner") String owner, @Named("repo") String repo);
}
static class Contributor {
@ -54,48 +48,9 @@ public class GitHubExample { @@ -54,48 +48,9 @@ public class GitHubExample {
for (Contributor contributor : contributors) {
System.out.println(contributor.login + " (" + contributor.contributions + ")");
}
System.out.println("Let's treat our contributors as an observable.");
Observable<Contributor> observable = github.observable("netflix", "feign");
CountDownLatch latch = new CountDownLatch(2);
System.out.println("Let's add 2 subscribers.");
observable.subscribe(new ContributorObserver(latch));
observable.subscribe(new ContributorObserver(latch));
// wait for the task to complete.
latch.await();
System.exit(0);
}
static class ContributorObserver implements Observer<Contributor> {
private final CountDownLatch latch;
public int count;
public ContributorObserver(CountDownLatch latch) {
this.latch = latch;
}
// parsed directly from the text stream without an intermediate collection.
@Override public void onNext(Contributor contributor) {
count++;
}
@Override public void onSuccess() {
System.out.println("found " + count + " contributors");
latch.countDown();
}
@Override public void onFailure(Throwable cause) {
cause.printStackTrace();
latch.countDown();
}
}
@Module(overrides = true, library = true)
@Module(overrides = true, library = true, includes = GsonModule.class)
static class LogToStderr {
@Provides Logger.Level loggingLevel() {

4
example-wikipedia/build.gradle

@ -1,8 +1,8 @@ @@ -1,8 +1,8 @@
apply plugin: 'java'
dependencies {
compile 'com.netflix.feign:feign-core:4.3.0'
compile 'com.netflix.feign:feign-gson:4.3.0'
compile 'com.netflix.feign:feign-core:5.0.0'
compile 'com.netflix.feign:feign-gson:5.0.0'
provided 'com.squareup.dagger:dagger-compiler:1.1.0'
}

15
example-wikipedia/src/main/java/feign/example/wikipedia/ResponseDecoder.java → example-wikipedia/src/main/java/feign/example/wikipedia/ResponseAdapter.java

@ -1,13 +1,12 @@ @@ -1,13 +1,12 @@
package feign.example.wikipedia;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import feign.codec.Decoder;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.Type;
abstract class ResponseDecoder<X> implements Decoder.TextStream<WikipediaExample.Response<X>> {
abstract class ResponseAdapter<X> extends TypeAdapter<WikipediaExample.Response<X>> {
/**
* name of the key inside the {@code query} dict which holds the elements desired. ex. {@code pages}.
@ -35,9 +34,8 @@ abstract class ResponseDecoder<X> implements Decoder.TextStream<WikipediaExample @@ -35,9 +34,8 @@ abstract class ResponseDecoder<X> implements Decoder.TextStream<WikipediaExample
* the wikipedia api doesn't use json arrays, rather a series of nested objects.
*/
@Override
public WikipediaExample.Response<X> decode(Reader ireader, Type type) throws IOException {
public WikipediaExample.Response<X> read(JsonReader reader) throws IOException {
WikipediaExample.Response<X> pages = new WikipediaExample.Response<X>();
JsonReader reader = new JsonReader(ireader);
reader.beginObject();
while (reader.hasNext()) {
String nextName = reader.nextName();
@ -84,4 +82,9 @@ abstract class ResponseDecoder<X> implements Decoder.TextStream<WikipediaExample @@ -84,4 +82,9 @@ abstract class ResponseDecoder<X> implements Decoder.TextStream<WikipediaExample
reader.close();
return pages;
}
@Override
public void write(JsonWriter out, WikipediaExample.Response<X> response) throws IOException {
throw new UnsupportedOperationException();
}
}

10
example-wikipedia/src/main/java/feign/example/wikipedia/WikipediaExample.java

@ -15,13 +15,13 @@ @@ -15,13 +15,13 @@
*/
package feign.example.wikipedia;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import dagger.Module;
import dagger.Provides;
import feign.Feign;
import feign.Logger;
import feign.RequestLine;
import feign.codec.Decoder;
import feign.gson.GsonModule;
import javax.inject.Named;
@ -101,14 +101,14 @@ public class WikipediaExample { @@ -101,14 +101,14 @@ public class WikipediaExample {
};
}
@Module(library = true, includes = GsonModule.class)
@Module(includes = GsonModule.class)
static class WikipediaDecoder {
/**
* add to the set of Decoders one that handles {@code Response<Page>}.
* registers a gson {@link TypeAdapter} for {@code Response<Page>}.
*/
@Provides(type = SET) Decoder pagesDecoder() {
return new ResponseDecoder<Page>() {
@Provides(type = SET) TypeAdapter pagesAdapter() {
return new ResponseAdapter<Page>() {
@Override
protected String query() {

2
gradle.properties

@ -1 +1 @@ @@ -1 +1 @@
version=5.0.0-SNAPSHOT
version=5.4.2-SNAPSHOT

13
gson/README.md

@ -1,9 +1,18 @@ @@ -1,9 +1,18 @@
Gson Codec
===================
This module adds support for encoding and decoding json via the Gson library.
This module adds support for encoding and decoding JSON via the Gson library.
Add this to your object graph like so:
Add `GsonEncoder` and/or `GsonDecoder` to your `Feign.Builder` like so:
```java
GitHub github = Feign.builder()
.encoder(new GsonEncoder())
.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());

54
gson/src/main/java/feign/gson/DoubleToIntMapTypeAdapter.java

@ -0,0 +1,54 @@ @@ -0,0 +1,54 @@
/*
* 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.InstanceCreator;
import com.google.gson.TypeAdapter;
import com.google.gson.internal.ConstructorConstructor;
import com.google.gson.internal.bind.MapTypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.Map;
/**
* Deals with scenario where Gson Object type treats all numbers as doubles.
*/
public class DoubleToIntMapTypeAdapter extends TypeAdapter<Map<String, Object>> {
final static TypeToken<Map<String, Object>> token = new TypeToken<Map<String, Object>>() {};
private final TypeAdapter<Map<String, Object>> delegate = new MapTypeAdapterFactory(new ConstructorConstructor(
Collections.<Type, InstanceCreator<?>>emptyMap()), false).create(new Gson(), token);
@Override public void write(JsonWriter out, Map<String, Object> value) throws IOException {
delegate.write(out, value);
}
@Override public Map<String, Object> read(JsonReader in) throws IOException {
Map<String, Object> map = delegate.read(in);
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (entry.getValue() instanceof Double) {
entry.setValue(Double.class.cast(entry.getValue()).intValue());
}
}
return map;
}
}

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

@ -0,0 +1,37 @@ @@ -0,0 +1,37 @@
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);
}
}

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

@ -0,0 +1,56 @@ @@ -0,0 +1,56 @@
/*
* 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.JsonIOException;
import feign.Response;
import feign.codec.Decoder;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.Type;
import static feign.Util.ensureClosed;
public class GsonDecoder implements Decoder {
private final Gson gson;
public GsonDecoder() {
this(new Gson());
}
public GsonDecoder(Gson gson) {
this.gson = gson;
}
@Override public Object decode(Response response, Type type) throws IOException {
if (response.body() == null) {
return null;
}
Reader reader = response.body().asReader();
try {
return gson.fromJson(reader, type);
} catch (JsonIOException e) {
if (e.getCause() != null && e.getCause() instanceof IOException) {
throw IOException.class.cast(e.getCause());
}
throw e;
} finally {
ensureClosed(reader);
}
}
}

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

@ -0,0 +1,36 @@ @@ -0,0 +1,36 @@
/*
* 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 feign.RequestTemplate;
import feign.codec.Encoder;
public class GsonEncoder implements Encoder {
private final Gson gson;
public GsonEncoder() {
this(new Gson());
}
public GsonEncoder(Gson gson) {
this.gson = gson;
}
@Override public void encode(Object object, RequestTemplate template) {
template.body(gson.toJson(object));
}
}

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

@ -17,31 +17,17 @@ package feign.gson; @@ -17,31 +17,17 @@ package feign.gson;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.InstanceCreator;
import com.google.gson.JsonIOException;
import com.google.gson.TypeAdapter;
import com.google.gson.internal.ConstructorConstructor;
import com.google.gson.internal.bind.MapTypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import dagger.Provides;
import feign.Feign;
import feign.RequestTemplate;
import feign.Response;
import feign.codec.Decoder;
import feign.codec.Encoder;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import static feign.Util.ensureClosed;
import static feign.Util.resolveLastTypeParameter;
/**
@ -52,10 +38,10 @@ import static feign.Util.resolveLastTypeParameter; @@ -52,10 +38,10 @@ import static feign.Util.resolveLastTypeParameter;
* 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() {
@ -83,47 +69,12 @@ import static feign.Util.resolveLastTypeParameter; @@ -83,47 +69,12 @@ import static feign.Util.resolveLastTypeParameter;
@dagger.Module(injects = Feign.class, addsTo = Feign.Defaults.class)
public final class GsonModule {
@Provides Encoder encoder(GsonCodec codec) {
return codec;
}
@Provides Decoder decoder(GsonCodec codec) {
return codec;
}
static class GsonCodec implements Encoder, Decoder {
private final Gson gson;
@Inject GsonCodec(Gson gson) {
this.gson = gson;
}
@Override public void encode(Object object, RequestTemplate template) {
template.body(gson.toJson(object));
@Provides Encoder encoder(Gson gson) {
return new GsonEncoder(gson);
}
@Override public Object decode(Response response, Type type) throws IOException {
if (void.class.equals(type) || response.body() == null) {
return null;
}
Reader reader = response.body().asReader();
try {
return fromJson(new JsonReader(reader), type);
} finally {
ensureClosed(reader);
}
}
private Object fromJson(JsonReader jsonReader, Type type) throws IOException {
try {
return gson.fromJson(jsonReader, type);
} catch (JsonIOException e) {
if (e.getCause() != null && e.getCause() instanceof IOException) {
throw IOException.class.cast(e.getCause());
}
throw e;
}
}
@Provides Decoder decoder(Gson gson) {
return new GsonDecoder(gson);
}
@Provides @Singleton Gson gson(Set<TypeAdapter> adapters) {
@ -135,30 +86,7 @@ public final class GsonModule { @@ -135,30 +86,7 @@ public final class GsonModule {
return builder.create();
}
// deals with scenario where gson Object type treats all numbers as doubles.
@Provides(type = Provides.Type.SET) TypeAdapter doubleToInt() {
return new TypeAdapter<Map<String, Object>>() {
TypeAdapter<Map<String, Object>> delegate = new MapTypeAdapterFactory(new ConstructorConstructor(
Collections.<Type, InstanceCreator<?>>emptyMap()), false).create(new Gson(), token);
@Override
public void write(JsonWriter out, Map<String, Object> value) throws IOException {
delegate.write(out, value);
@Provides(type = Provides.Type.SET_VALUES) Set<TypeAdapter> noDefaultTypeAdapters() {
return Collections.emptySet();
}
@Override
public Map<String, Object> read(JsonReader in) throws IOException {
Map<String, Object> map = delegate.read(in);
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (entry.getValue() instanceof Double) {
entry.setValue(Double.class.cast(entry.getValue()).intValue());
}
}
return map;
}
}.nullSafe();
}
private final static TypeToken<Map<String, Object>> token = new TypeToken<Map<String, Object>>() {
};
}

12
gson/src/test/java/feign/gson/GsonModuleTest.java

@ -52,8 +52,8 @@ public class GsonModuleTest { @@ -52,8 +52,8 @@ public class GsonModuleTest {
EncoderAndDecoderBindings bindings = new EncoderAndDecoderBindings();
ObjectGraph.create(bindings).inject(bindings);
assertEquals(bindings.encoder.getClass(), GsonModule.GsonCodec.class);
assertEquals(bindings.decoder.getClass(), GsonModule.GsonCodec.class);
assertEquals(bindings.encoder.getClass(), GsonEncoder.class);
assertEquals(bindings.decoder.getClass(), GsonDecoder.class);
}
@Module(includes = GsonModule.class, injects = EncoderBindings.class)
@ -133,14 +133,6 @@ public class GsonModuleTest { @@ -133,14 +133,6 @@ public class GsonModuleTest {
}.getType()), zones);
}
@Test public void voidDecodesToNull() throws Exception {
DecoderBindings bindings = new DecoderBindings();
ObjectGraph.create(bindings).inject(bindings);
Response response = Response.create(200, "OK", Collections.<String, Collection<String>>emptyMap(), zonesJson);
assertEquals(bindings.decoder.decode(response, void.class), null);
}
@Test public void nullBodyDecodesToNull() throws Exception {
DecoderBindings bindings = new DecoderBindings();
ObjectGraph.create(bindings).inject(bindings);

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

@ -17,7 +17,7 @@ package feign.gson.examples; @@ -17,7 +17,7 @@ package feign.gson.examples;
import feign.Feign;
import feign.RequestLine;
import feign.gson.GsonModule;
import feign.gson.GsonDecoder;
import javax.inject.Named;
import java.util.List;
@ -38,7 +38,7 @@ public class GitHubExample { @@ -38,7 +38,7 @@ public class GitHubExample {
}
public static void main(String... args) throws InterruptedException {
GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule());
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");

33
jackson/README.md

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
Jackson Codec
===================
This module adds support for encoding and decoding JSON via Jackson.
Add `JacksonEncoder` and/or `JacksonDecoder` to your `Feign.Builder` like so:
```java
GitHub github = Feign.builder()
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.target(GitHub.class, "https://api.github.com");
```
If you want to customize the `ObjectMapper` that is used, provide it to the `JacksonEncoder` and `JacksonDecoder`:
```java
ObjectMapper mapper = new ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.configure(SerializationFeature.INDENT_OUTPUT, true)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
GitHub github = Feign.builder()
.encoder(new JacksonEncoder(mapper))
.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());
```

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

@ -0,0 +1,53 @@ @@ -0,0 +1,53 @@
/*
* 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.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.RuntimeJsonMappingException;
import feign.Response;
import feign.codec.Decoder;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.Type;
public class JacksonDecoder implements Decoder {
private final ObjectMapper mapper;
public JacksonDecoder() {
this(new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false));
}
public JacksonDecoder(ObjectMapper mapper) {
this.mapper = mapper;
}
@Override public Object decode(Response response, Type type) throws IOException {
if (response.body() == null) {
return null;
}
Reader reader = response.body().asReader();
try {
return mapper.readValue(reader, mapper.constructType(type));
} catch (RuntimeJsonMappingException e) {
if (e.getCause() != null && e.getCause() instanceof IOException) {
throw IOException.class.cast(e.getCause());
}
throw e;
}
}
}

46
jackson/src/main/java/feign/jackson/JacksonEncoder.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.jackson;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import feign.RequestTemplate;
import feign.codec.EncodeException;
import feign.codec.Encoder;
public class JacksonEncoder implements Encoder {
private final ObjectMapper mapper;
public JacksonEncoder() {
this(new ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.configure(SerializationFeature.INDENT_OUTPUT, true));
}
public JacksonEncoder(ObjectMapper mapper) {
this.mapper = mapper;
}
@Override public void encode(Object object, RequestTemplate template) throws EncodeException {
try {
template.body(mapper.writeValueAsString(object));
} catch (JsonProcessingException e) {
throw new EncodeException(e.getMessage(), e);
}
}
}

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

@ -0,0 +1,103 @@ @@ -0,0 +1,103 @@
/*
* 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();
}
}

184
jackson/src/test/java/feign/jackson/JacksonModuleTest.java

@ -0,0 +1,184 @@ @@ -0,0 +1,184 @@
package feign.jackson;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.google.common.reflect.TypeToken;
import dagger.Module;
import dagger.ObjectGraph;
import dagger.Provides;
import feign.RequestTemplate;
import feign.Response;
import feign.codec.Decoder;
import feign.codec.Encoder;
import org.testng.annotations.Test;
import javax.inject.Inject;
import java.io.IOException;
import java.util.*;
import static org.testng.Assert.assertEquals;
@Test
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(bindings.encoder.getClass(), JacksonEncoder.class);
assertEquals(bindings.decoder.getClass(), JacksonDecoder.class);
}
@Module(includes = JacksonModule.class, injects = EncoderBindings.class)
static class EncoderBindings {
@Inject Encoder encoder;
}
@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);
assertEquals(template.body(), ""//
+ "{\n" //
+ " \"foo\" : 1\n" //
+ "}");
}
@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);
assertEquals(template.body(), ""//
+ "{\n" //
+ " \"foo\" : 1,\n" //
+ " \"bar\" : [ 2, 3 ]\n" //
+ "}");
}
static class Zone extends LinkedHashMap<String, Object> {
Zone() {
// for reflective instantiation.
}
Zone(String name) {
this(name, null);
}
Zone(String name, String id) {
put("name", name);
if (id != null) {
put("id", id);
}
}
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);
assertEquals(bindings.decoder.decode(response, new TypeToken<List<Zone>>() {
}.getType()), zones);
}
@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(), null);
assertEquals(bindings.decoder.decode(response, String.class), null);
}
private String zonesJson = ""//
+ "[\n"//
+ " {\n"//
+ " \"name\": \"denominator.io.\"\n"//
+ " },\n"//
+ " {\n"//
+ " \"name\": \"denominator.io.\",\n"//
+ " \"id\": \"ABCD\"\n"//
+ " }\n"//
+ "]\n";
static class ZoneDeserializer extends StdDeserializer<Zone> {
public ZoneDeserializer() {
super(Zone.class);
}
@Override
public Zone deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
Zone zone = new Zone();
jp.nextToken();
while (jp.nextToken() != JsonToken.END_OBJECT) {
String name = jp.getCurrentName();
String value = jp.getValueAsString();
if (value != null) {
zone.put(name, value.toUpperCase());
}
}
return zone;
}
}
static class ZoneModule extends SimpleModule {
public ZoneModule() {
addDeserializer(Zone.class, new ZoneDeserializer());
}
}
@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);
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);
assertEquals(bindings.decoder.decode(response, new TypeToken<List<Zone>>() {
}.getType()), zones);
}
}

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

@ -0,0 +1,40 @@ @@ -0,0 +1,40 @@
package feign.jackson.examples;
import feign.Feign;
import feign.RequestLine;
import feign.jackson.JacksonDecoder;
import javax.inject.Named;
import java.util.List;
/**
* adapted from {@code com.example.retrofit.GitHubClient}
*/
public class GitHubExample {
interface GitHub {
@RequestLine("GET /repos/{owner}/{repo}/contributors")
List<Contributor> contributors(@Named("owner") String owner, @Named("repo") String repo);
}
static class Contributor {
private String login;
private int contributions;
void setLogin(String login) {
this.login = login;
}
void setContributions(int contributions) {
this.contributions = contributions;
}
}
public static void main(String... args) throws InterruptedException {
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) {
System.out.println(contributor.login + " (" + contributor.contributions + ")");
}
}
}

15
ribbon/src/main/java/feign/ribbon/LBClient.java

@ -62,11 +62,11 @@ class LBClient extends AbstractLoadBalancerAwareClient<LBClient.RibbonRequest, L @@ -62,11 +62,11 @@ class LBClient extends AbstractLoadBalancerAwareClient<LBClient.RibbonRequest, L
return new RibbonResponse(request.getUri(), response);
}
@Override protected boolean isCircuitBreakerException(Exception e) {
@Override protected boolean isCircuitBreakerException(Throwable e) {
return e instanceof IOException;
}
@Override protected boolean isRetriableException(Exception e) {
@Override protected boolean isRetriableException(Throwable e) {
return e instanceof RetryableException;
}
@ -75,10 +75,6 @@ class LBClient extends AbstractLoadBalancerAwareClient<LBClient.RibbonRequest, L @@ -75,10 +75,6 @@ class LBClient extends AbstractLoadBalancerAwareClient<LBClient.RibbonRequest, L
return new Pair<String, Integer>(URI.create(task.request.url()).getScheme(), task.getUri().getPort());
}
@Override protected int getDefaultPort() {
return 443;
}
static class RibbonRequest extends ClientRequest implements Cloneable {
private final Request request;
@ -134,6 +130,13 @@ class LBClient extends AbstractLoadBalancerAwareClient<LBClient.RibbonRequest, L @@ -134,6 +130,13 @@ class LBClient extends AbstractLoadBalancerAwareClient<LBClient.RibbonRequest, L
Response toResponse() {
return response;
}
@Override
public void close() throws IOException {
if (response.body() != null) {
response.body().close();
}
}
}
static int config(RibbonRequest request, CommonClientConfigKey key, int defaultValue) {

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

@ -104,6 +104,8 @@ public class LoadBalancingTarget<T> implements Target<T> { @@ -104,6 +104,8 @@ public class LoadBalancingTarget<T> implements Target<T> {
}
@Override public boolean equals(Object obj) {
if (obj == null)
return false;
if (this == obj)
return true;
if (LoadBalancingTarget.class != obj.getClass())

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

@ -76,6 +76,9 @@ public class RibbonModule { @@ -76,6 +76,9 @@ public class RibbonModule {
LBClient.RibbonRequest ribbonRequest = new LBClient.RibbonRequest(request, uriWithoutSchemeAndPort);
return lbClient(clientName).executeWithLoadBalancer(ribbonRequest).toResponse();
} catch (ClientException e) {
if (e.getCause() instanceof IOException) {
throw IOException.class.cast(e.getCause());
}
throw Throwables.propagate(e);
}
}

5
ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java

@ -27,6 +27,7 @@ import feign.Feign; @@ -27,6 +27,7 @@ import feign.Feign;
import feign.RequestLine;
import static com.netflix.config.ConfigurationManager.getConfigInstance;
import static feign.Util.UTF_8;
import static org.testng.Assert.assertEquals;
@Test
@ -41,10 +42,10 @@ public class LoadBalancingTargetTest { @@ -41,10 +42,10 @@ public class LoadBalancingTargetTest {
String serverListKey = name + ".ribbon.listOfServers";
MockWebServer server1 = new MockWebServer();
server1.enqueue(new MockResponse().setResponseCode(200).setBody("success!".getBytes()));
server1.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8)));
server1.play();
MockWebServer server2 = new MockWebServer();
server2.enqueue(new MockResponse().setResponseCode(200).setBody("success!".getBytes()));
server2.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8)));
server2.play();
getConfigInstance().setProperty(serverListKey, hostAndPort(server1.getUrl("")) + "," + hostAndPort(server2.getUrl("")));

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

@ -17,6 +17,7 @@ package feign.ribbon; @@ -17,6 +17,7 @@ package feign.ribbon;
import com.google.mockwebserver.MockResponse;
import com.google.mockwebserver.MockWebServer;
import com.google.mockwebserver.SocketPolicy;
import dagger.Provides;
import feign.Feign;
import feign.RequestLine;
@ -28,6 +29,7 @@ import java.io.IOException; @@ -28,6 +29,7 @@ import java.io.IOException;
import java.net.URL;
import static com.netflix.config.ConfigurationManager.getConfigInstance;
import static feign.Util.UTF_8;
import static org.testng.Assert.assertEquals;
@Test
@ -35,7 +37,7 @@ public class RibbonClientTest { @@ -35,7 +37,7 @@ public class RibbonClientTest {
interface TestInterface {
@RequestLine("POST /") void post();
@dagger.Module(injects = Feign.class, addsTo = Feign.Defaults.class)
@dagger.Module(injects = Feign.class, overrides = true, addsTo = Feign.Defaults.class)
static class Module {
@Provides Decoder defaultDecoder() {
return new Decoder.Default();
@ -53,10 +55,10 @@ public class RibbonClientTest { @@ -53,10 +55,10 @@ public class RibbonClientTest {
String serverListKey = client + ".ribbon.listOfServers";
MockWebServer server1 = new MockWebServer();
server1.enqueue(new MockResponse().setResponseCode(200).setBody("success!".getBytes()));
server1.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8)));
server1.play();
MockWebServer server2 = new MockWebServer();
server2.enqueue(new MockResponse().setResponseCode(200).setBody("success!".getBytes()));
server2.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8)));
server2.play();
getConfigInstance().setProperty(serverListKey, hostAndPort(server1.getUrl("")) + "," + hostAndPort(server2.getUrl("")));
@ -79,6 +81,33 @@ public class RibbonClientTest { @@ -79,6 +81,33 @@ public class RibbonClientTest {
}
}
@Test
public void ioExceptionRetry() throws IOException, InterruptedException {
String client = "RibbonClientTest-ioExceptionRetry";
String serverListKey = client + ".ribbon.listOfServers";
MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START));
server.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8)));
server.play();
getConfigInstance().setProperty(serverListKey, hostAndPort(server.getUrl("")));
try {
TestInterface api = Feign.create(TestInterface.class, "http://" + client, new TestInterface.Module(), new RibbonModule());
api.post();
assertEquals(server.getRequestCount(), 2);
// TODO: verify ribbon stats match
// assertEquals(target.lb().getLoadBalancerStats().getSingleServerStat())
} finally {
server.shutdown();
getConfigInstance().clearProperty(serverListKey);
}
}
static String hostAndPort(URL url) {
// our build slaves have underscores in their hostnames which aren't permitted by ribbon
return "localhost:" + url.getPort();

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

@ -143,7 +143,7 @@ public class SAXDecoder implements Decoder { @@ -143,7 +143,7 @@ public class SAXDecoder implements Decoder {
@Override
public Object decode(Response response, Type type) throws IOException, DecodeException {
if (void.class.equals(type) || response.body() == null) {
if (response.body() == null) {
return null;
}
Provider<? extends ContentHandlerWithResult<?>> handlerProvider = handlerProviders.get(type);

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

@ -136,11 +136,6 @@ public class SAXDecoderTest { @@ -136,11 +136,6 @@ public class SAXDecoderTest {
}
}
@Test public void voidDecodesToNull() throws Exception {
Response response = Response.create(200, "OK", Collections.<String, Collection<String>>emptyMap(), statusFailed);
assertEquals(decoder.decode(response, void.class), null);
}
@Test public void nullBodyDecodesToNull() throws Exception {
Response response = Response.create(204, "OK", Collections.<String, Collection<String>>emptyMap(), null);
assertEquals(decoder.decode(response, String.class), null);

8
sax/src/test/java/feign/sax/examples/AWSSignatureVersion4.java

@ -60,7 +60,11 @@ public class AWSSignatureVersion4 implements Function<RequestTemplate, Request> @@ -60,7 +60,11 @@ public class AWSSignatureVersion4 implements Function<RequestTemplate, Request>
transform(input.headers().get(key), trimToLowercase));
}
String timestamp = iso8601.format(new Date());
String timestamp;
synchronized (iso8601) {
timestamp = iso8601.format(new Date());
}
String credentialScope = Joiner.on('/').join(timestamp.substring(0, 8), region, service, "aws4_request");
input.query("X-Amz-Algorithm", "AWS4-HMAC-SHA256");
@ -135,7 +139,7 @@ public class AWSSignatureVersion4 implements Function<RequestTemplate, Request> @@ -135,7 +139,7 @@ public class AWSSignatureVersion4 implements Function<RequestTemplate, Request>
private static final Function<String, String> trimToLowercase = new Function<String, String>() {
public String apply(String in) {
return in.toLowerCase().trim();
return in == null ? null : in.toLowerCase().trim();
}
};

2
settings.gradle

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
rootProject.name='feign'
include 'core', 'sax', 'gson', 'jaxrs', 'ribbon', 'example-github', 'example-wikipedia'
include 'core', 'sax', 'gson', 'jackson', 'jaxrs', 'ribbon', 'example-github', 'example-wikipedia'
rootProject.children.each { childProject ->
childProject.name = 'feign-' + childProject.name

Loading…
Cancel
Save