Browse Source

Squashed commit of the following:

commit 34eb5751c760cf1f11cdbab920d6a3a1c6f06640
Author: Matt Hurne <matt@thehurnes.com>
Date:   Tue Oct 15 15:54:20 2013 -0400

    Remove unnecessary defensive close of Reader

commit 38e51606750517d4a52571c408190e614a4a4834
Author: Matt Hurne <matt@thehurnes.com>
Date:   Tue Oct 8 13:59:35 2013 -0400

    Replace wildcard import with individual imports

commit cc845814ea677ba5920caf5a7a914010623caf1e
Author: Matt Hurne <matt@thehurnes.com>
Date:   Tue Oct 8 13:55:37 2013 -0400

    Revert GitHub example to use JacksonDecoder rather than JacksonModule now that JacksonDecoder behaves sensibly with its default ObjectMapper

commit 8b9638261afe2549c3a43238ee1b66d044f969f4
Author: Matt Hurne <matt@thehurnes.com>
Date:   Tue Oct 8 13:52:45 2013 -0400

    Configure default ObjectMapper used by JacksonEncoder and JacksonDecoder with sensible overrides of default behaviors

commit 0f275bf7574b66c20a0e6aefe0140f599638992f
Author: Matt Hurne <matt@thehurnes.com>
Date:   Tue Oct 8 13:18:26 2013 -0400

    Unwrap RuntimeJsonMappingExceptions caught in JacksonDecoder, since they are only ever used to wrap JsonMappingExceptions, which are IOExceptions.

commit 1b6995260a5727796e388bbb0b6c88b65e182415
Author: Matt Hurne <matt@thehurnes.com>
Date:   Tue Oct 8 13:09:44 2013 -0400

    Update Jackson integration README

commit add4007a59559e7b4e2accfa0b0a0215bab62cef
Author: Matt Hurne <matt@thehurnes.com>
Date:   Tue Oct 8 13:07:35 2013 -0400

    Update CHANGES and README to reflect addition of Jackson integration

commit 86c0fcfc704b1b8d03e5eaf69c608fc2761d617b
Author: Matt Hurne <matt@thehurnes.com>
Date:   Tue Oct 8 12:11:56 2013 -0400

    Update Jackson GitHub example to make use of JacksonModule, and to avoid the need for Jackson annotations

commit 1552b3f8239636da0f27ace3c7b42038536e5caf
Author: Matt Hurne <matt@thehurnes.com>
Date:   Tue Oct 8 12:05:56 2013 -0400

    Replace wildcard import with individual imports

commit 0b7cfd08516dfbf66f1a69263ed456f2c0671c76
Author: Matt Hurne <matt@thehurnes.com>
Date:   Tue Oct 8 11:01:11 2013 -0400

    Initial implementation of Jackson codec

    This new codec may be used as an alternative to Gson.

commit 94027ec3319f5145c0e18ef472d8e928e97a9527
Author: Matt Hurne <matt@thehurnes.com>
Date:   Tue Oct 8 08:31:14 2013 -0400

    Improve EncodeException and DecodeException Javadoc comments
pull/89/head
Matt Hurne 11 years ago committed by adrian
parent
commit
2e296883c9
  1. 1
      CHANGES.md
  2. 12
      README.md
  3. 15
      build.gradle
  4. 2
      core/src/main/java/feign/codec/DecodeException.java
  5. 4
      core/src/main/java/feign/codec/EncodeException.java
  6. 33
      jackson/README.md
  7. 53
      jackson/src/main/java/feign/jackson/JacksonDecoder.java
  8. 46
      jackson/src/main/java/feign/jackson/JacksonEncoder.java
  9. 103
      jackson/src/main/java/feign/jackson/JacksonModule.java
  10. 184
      jackson/src/test/java/feign/jackson/JacksonModuleTest.java
  11. 40
      jackson/src/test/java/feign/jackson/examples/GitHubExample.java
  12. 2
      settings.gradle

1
CHANGES.md

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
### 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`

12
README.md

@ -99,6 +99,18 @@ GitHub github = Feign.builder() @@ -99,6 +99,18 @@ GitHub github = Feign.builder()
.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.builder()
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.target(GitHub.class, "https://api.github.com");
```
### Sax
[SaxDecoder](https://github.com/Netflix/feign/tree/master/sax) allows you to decode XML in a way that is compatible with normal JVM and also Android environments.

15
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'

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 {

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 {

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 + ")");
}
}
}

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