Browse Source
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 commentspull/82/merge
Matt Hurne
11 years ago
committed by
adrian
12 changed files with 491 additions and 4 deletions
@ -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()); |
||||
``` |
@ -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; |
||||
} |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
||||
} |
@ -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<ObjectId> { |
||||
* public ObjectIdSerializer() { |
||||
* super(ObjectId.class); |
||||
* } |
||||
* |
||||
* @Override |
||||
* public void serialize(ObjectId value, JsonGenerator jsonGenerator, SerializerProvider provider) throws IOException { |
||||
* jsonGenerator.writeString(value.toString()); |
||||
* } |
||||
* } |
||||
* |
||||
* public class ObjectIdDeserializer extends StdDeserializer<ObjectId> { |
||||
* public ObjectIdDeserializer() { |
||||
* super(ObjectId.class); |
||||
* } |
||||
* |
||||
* @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()); |
||||
* } |
||||
* } |
||||
* |
||||
* @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(); |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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 + ")"); |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue