diff --git a/CHANGES.md b/CHANGES.md index 833a455239..00f5e219f8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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` diff --git a/README.md b/README.md index 1d43eacd3f..e2a56ef1ed 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/build.gradle b/build.gradle index 022cce7e8c..3da4379f3c 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/core/src/main/java/feign/codec/DecodeException.java b/core/src/main/java/feign/codec/DecodeException.java index 5efab25ba6..1671bbdb60 100644 --- a/core/src/main/java/feign/codec/DecodeException.java +++ b/core/src/main/java/feign/codec/DecodeException.java @@ -22,7 +22,7 @@ /** * 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 { diff --git a/core/src/main/java/feign/codec/EncodeException.java b/core/src/main/java/feign/codec/EncodeException.java index 12d06ba340..bc9c660ca0 100644 --- a/core/src/main/java/feign/codec/EncodeException.java +++ b/core/src/main/java/feign/codec/EncodeException.java @@ -21,8 +21,8 @@ /** * 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 { diff --git a/jackson/README.md b/jackson/README.md new file mode 100644 index 0000000000..a6b8f0fcdc --- /dev/null +++ b/jackson/README.md @@ -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()); +``` diff --git a/jackson/src/main/java/feign/jackson/JacksonDecoder.java b/jackson/src/main/java/feign/jackson/JacksonDecoder.java new file mode 100644 index 0000000000..83400afc14 --- /dev/null +++ b/jackson/src/main/java/feign/jackson/JacksonDecoder.java @@ -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; + } + } +} diff --git a/jackson/src/main/java/feign/jackson/JacksonEncoder.java b/jackson/src/main/java/feign/jackson/JacksonEncoder.java new file mode 100644 index 0000000000..1cc6895f2b --- /dev/null +++ b/jackson/src/main/java/feign/jackson/JacksonEncoder.java @@ -0,0 +1,46 @@ +/* + * Copyright 2013 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign.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); + } + } +} diff --git a/jackson/src/main/java/feign/jackson/JacksonModule.java b/jackson/src/main/java/feign/jackson/JacksonModule.java new file mode 100644 index 0000000000..7826118afa --- /dev/null +++ b/jackson/src/main/java/feign/jackson/JacksonModule.java @@ -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; + +/** + *
+ * 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();
+ * }
+ *
+ */
+@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