diff --git a/jackson3/README.md b/jackson3/README.md new file mode 100644 index 0000000000..0a1107020b --- /dev/null +++ b/jackson3/README.md @@ -0,0 +1,39 @@ +Jackson 3 Codec +=================== + +This module adds support for encoding and decoding JSON via Jackson 3. + +**Note:** Jackson 3 requires Java 17 or higher. + +Add `Jackson3Encoder` and/or `Jackson3Decoder` to your `Feign.Builder` like so: + +```java +GitHub github = Feign.builder() + .encoder(new Jackson3Encoder()) + .decoder(new Jackson3Decoder()) + .target(GitHub.class, "https://api.github.com"); +``` + +If you want to customize the `JsonMapper` that is used, provide it to the `Jackson3Encoder` and `Jackson3Decoder`: + +```java +JsonMapper mapper = JsonMapper.builder() + .changeDefaultPropertyInclusion(incl -> incl.withValueInclusion(JsonInclude.Include.NON_NULL)) + .enable(SerializationFeature.INDENT_OUTPUT) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .build(); + +GitHub github = Feign.builder() + .encoder(new Jackson3Encoder(mapper)) + .decoder(new Jackson3Decoder(mapper)) + .target(GitHub.class, "https://api.github.com"); +``` + +## Migration from Jackson 2 to Jackson 3 + +The main differences are: + +- Package changes: `com.fasterxml.jackson` → `tools.jackson` (except for `com.fasterxml.jackson.annotation`) +- GroupId changes: `com.fasterxml.jackson.core` → `tools.jackson.core` +- `ObjectMapper` is immutable and must be configured via `JsonMapper.builder()` +- Java 17 minimum requirement diff --git a/jackson3/pom.xml b/jackson3/pom.xml new file mode 100644 index 0000000000..de7e9225cd --- /dev/null +++ b/jackson3/pom.xml @@ -0,0 +1,66 @@ + + + + 4.0.0 + + + io.github.openfeign + feign-parent + 13.7-SNAPSHOT + + + feign-jackson3 + Feign Jackson 3 + Feign Jackson 3 + + + 17 + + + + + + tools.jackson + jackson-bom + ${jackson3.version} + pom + import + + + + + + + ${project.groupId} + feign-core + + + + tools.jackson.core + jackson-databind + + + + ${project.groupId} + feign-core + test-jar + test + + + diff --git a/jackson3/src/main/java/feign/jackson3/Jackson3Decoder.java b/jackson3/src/main/java/feign/jackson3/Jackson3Decoder.java new file mode 100644 index 0000000000..723f0b146f --- /dev/null +++ b/jackson3/src/main/java/feign/jackson3/Jackson3Decoder.java @@ -0,0 +1,74 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * 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.jackson3; + +import feign.Response; +import feign.Util; +import feign.codec.Decoder; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Reader; +import java.lang.reflect.Type; +import java.util.Collections; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.JacksonModule; +import tools.jackson.databind.json.JsonMapper; + +public class Jackson3Decoder implements Decoder { + + private final JsonMapper mapper; + + public Jackson3Decoder() { + this(Collections.emptyList()); + } + + public Jackson3Decoder(Iterable modules) { + this( + JsonMapper.builder() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .addModules(modules) + .build()); + } + + public Jackson3Decoder(JsonMapper mapper) { + this.mapper = mapper; + } + + @Override + public Object decode(Response response, Type type) throws IOException { + if (response.status() == 404 || response.status() == 204) return Util.emptyValueOf(type); + if (response.body() == null) return null; + Reader reader = response.body().asReader(response.charset()); + if (!reader.markSupported()) { + reader = new BufferedReader(reader, 1); + } + try { + // Read the first byte to see if we have any data + reader.mark(1); + if (reader.read() == -1) { + return null; // Eagerly returning null avoids "No content to map due to end-of-input" + } + reader.reset(); + return mapper.readValue(reader, mapper.constructType(type)); + } catch (JacksonException e) { + if (e.getCause() != null && e.getCause() instanceof IOException) { + throw IOException.class.cast(e.getCause()); + } + throw e; + } + } +} diff --git a/jackson3/src/main/java/feign/jackson3/Jackson3Encoder.java b/jackson3/src/main/java/feign/jackson3/Jackson3Encoder.java new file mode 100644 index 0000000000..d6455000a0 --- /dev/null +++ b/jackson3/src/main/java/feign/jackson3/Jackson3Encoder.java @@ -0,0 +1,62 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * 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.jackson3; + +import com.fasterxml.jackson.annotation.JsonInclude; +import feign.RequestTemplate; +import feign.Util; +import feign.codec.EncodeException; +import feign.codec.Encoder; +import java.lang.reflect.Type; +import java.util.Collections; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.JacksonModule; +import tools.jackson.databind.JavaType; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.json.JsonMapper; + +public class Jackson3Encoder implements Encoder { + + private final JsonMapper mapper; + + public Jackson3Encoder() { + this(Collections.emptyList()); + } + + public Jackson3Encoder(Iterable modules) { + this( + JsonMapper.builder() + .changeDefaultPropertyInclusion( + incl -> incl.withValueInclusion(JsonInclude.Include.NON_NULL)) + .enable(SerializationFeature.INDENT_OUTPUT) + .addModules(modules) + .build()); + } + + public Jackson3Encoder(JsonMapper mapper) { + this.mapper = mapper; + } + + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) { + try { + JavaType javaType = mapper.getTypeFactory().constructType(bodyType); + template.body(mapper.writerFor(javaType).writeValueAsBytes(object), Util.UTF_8); + } catch (JacksonException e) { + throw new EncodeException(e.getMessage(), e); + } + } +} diff --git a/jackson3/src/main/java/feign/jackson3/Jackson3IteratorDecoder.java b/jackson3/src/main/java/feign/jackson3/Jackson3IteratorDecoder.java new file mode 100644 index 0000000000..9ba52fa593 --- /dev/null +++ b/jackson3/src/main/java/feign/jackson3/Jackson3IteratorDecoder.java @@ -0,0 +1,199 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * 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.jackson3; + +import static feign.Util.UTF_8; +import static feign.Util.ensureClosed; + +import feign.Response; +import feign.Util; +import feign.codec.DecodeException; +import feign.codec.Decoder; +import java.io.BufferedReader; +import java.io.Closeable; +import java.io.IOException; +import java.io.Reader; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.Iterator; +import java.util.NoSuchElementException; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.JacksonModule; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.json.JsonMapper; + +/** + * Jackson 3 decoder which return a closeable iterator. Returned iterator auto-close the {@code + * Response} when it reached json array end or failed to parse stream. If this iterator is not + * fetched till the end, it has to be casted to {@code Closeable} and explicity {@code + * Closeable#close} by the consumer. + * + *

+ * + *

+ * + *

Example:
+ * + *

+ * 
+ * Feign.builder()
+ *   .decoder(Jackson3IteratorDecoder.create())
+ *   .doNotCloseAfterDecode() // Required to fetch the iterator after the response is processed, need to be close
+ *   .target(GitHub.class, "https://api.github.com");
+ * interface GitHub {
+ *  {@literal @}RequestLine("GET /repos/{owner}/{repo}/contributors")
+ *   Iterator contributors(@Param("owner") String owner, @Param("repo") String repo);
+ * }
+ * 
+ */ +public final class Jackson3IteratorDecoder implements Decoder { + + private final JsonMapper mapper; + + Jackson3IteratorDecoder(JsonMapper mapper) { + this.mapper = mapper; + } + + @Override + public Object decode(Response response, Type type) throws IOException { + if (response.status() == 404 || response.status() == 204) return Util.emptyValueOf(type); + if (response.body() == null) return null; + Reader reader = response.body().asReader(UTF_8); + if (!reader.markSupported()) { + reader = new BufferedReader(reader, 1); + } + try { + // Read the first byte to see if we have any data + reader.mark(1); + if (reader.read() == -1) { + return null; // Eagerly returning null avoids "No content to map due to end-of-input" + } + reader.reset(); + return new Jackson3Iterator( + actualIteratorTypeArgument(type), mapper, response, reader); + } catch (JacksonException e) { + if (e.getCause() != null && e.getCause() instanceof IOException) { + throw IOException.class.cast(e.getCause()); + } + throw e; + } + } + + private static Type actualIteratorTypeArgument(Type type) { + if (!(type instanceof ParameterizedType)) { + throw new IllegalArgumentException("Not supported type " + type.toString()); + } + ParameterizedType parameterizedType = (ParameterizedType) type; + if (!Iterator.class.equals(parameterizedType.getRawType())) { + throw new IllegalArgumentException( + "Not an iterator type " + parameterizedType.getRawType().toString()); + } + return ((ParameterizedType) type).getActualTypeArguments()[0]; + } + + public static Jackson3IteratorDecoder create() { + return create(Collections.emptyList()); + } + + public static Jackson3IteratorDecoder create(Iterable modules) { + return new Jackson3IteratorDecoder( + JsonMapper.builder() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + // Disable FAIL_ON_TRAILING_TOKENS for iterator: we read a JSON array element by + // element, so there are always "trailing tokens" (the remaining array elements) + .disable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS) + .addModules(modules) + .build()); + } + + public static Jackson3IteratorDecoder create(JsonMapper jsonMapper) { + return new Jackson3IteratorDecoder(jsonMapper); + } + + static final class Jackson3Iterator implements Iterator, Closeable { + private final Response response; + private final JsonParser parser; + private final ObjectReader objectReader; + + private T current; + + Jackson3Iterator(Type type, JsonMapper mapper, Response response, Reader reader) + throws IOException { + this.response = response; + this.parser = mapper.createParser(reader); + this.objectReader = mapper.reader().forType(mapper.constructType(type)); + } + + @Override + public boolean hasNext() { + if (current == null) { + current = readNext(); + } + return current != null; + } + + private T readNext() { + try { + JsonToken jsonToken = parser.nextToken(); + if (jsonToken == null) { + return null; + } + + if (jsonToken == JsonToken.START_ARRAY) { + jsonToken = parser.nextToken(); + } + + if (jsonToken == JsonToken.END_ARRAY) { + ensureClosed(this); + return null; + } + + return objectReader.readValue(parser); + } catch (JacksonException e) { + // Input Stream closed automatically by parser + throw new DecodeException(response.status(), e.getMessage(), response.request(), e); + } + } + + @Override + public T next() { + if (current != null) { + T tmp = current; + current = null; + return tmp; + } + T next = readNext(); + if (next == null) { + throw new NoSuchElementException(); + } + return next; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + @Override + public void close() throws IOException { + ensureClosed(this.response); + } + } +} diff --git a/jackson3/src/test/java/feign/jackson3/Jackson3CodecTest.java b/jackson3/src/test/java/feign/jackson3/Jackson3CodecTest.java new file mode 100644 index 0000000000..d91683033c --- /dev/null +++ b/jackson3/src/test/java/feign/jackson3/Jackson3CodecTest.java @@ -0,0 +1,388 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * 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.jackson3; + +import static feign.Util.UTF_8; +import static feign.assertj.FeignAssertions.assertThat; +import static org.assertj.core.api.Assertions.assertThat; + +import feign.Request; +import feign.Request.HttpMethod; +import feign.RequestTemplate; +import feign.Response; +import feign.Util; +import java.io.Closeable; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.deser.std.StdDeserializer; +import tools.jackson.databind.module.SimpleModule; +import tools.jackson.databind.ser.std.StdSerializer; + +@SuppressWarnings("deprecation") +class Jackson3CodecTest { + + private String zonesJson = + "" // + + "[" + + System.lineSeparator() // + + " {" + + System.lineSeparator() // + + " \"name\": \"denominator.io.\"" + + System.lineSeparator() // + + " }," + + System.lineSeparator() // + + " {" + + System.lineSeparator() // + + " \"name\": \"denominator.io.\"," + + System.lineSeparator() // + + " \"id\": \"ABCD\"" + + System.lineSeparator() // + + " }" + + System.lineSeparator() // + + "]" + + System.lineSeparator(); + + @Test + void encodesMapObjectNumericalValuesAsInteger() { + Map map = new LinkedHashMap<>(); + map.put("foo", 1); + + RequestTemplate template = new RequestTemplate(); + new Jackson3Encoder().encode(map, map.getClass(), template); + + assertThat(template) + .hasBody( + "" // + + "{" + + System.lineSeparator() // + + " \"foo\" : 1" + + System.lineSeparator() // + + "}"); + } + + @Test + void encodesFormParams() { + Map form = new LinkedHashMap<>(); + form.put("foo", 1); + form.put("bar", Arrays.asList(2, 3)); + + RequestTemplate template = new RequestTemplate(); + new Jackson3Encoder().encode(form, new TypeReference>() {}.getType(), template); + + assertThat(template) + .hasBody( + "" // + + "{" + + System.lineSeparator() // + + " \"foo\" : 1," + + System.lineSeparator() // + + " \"bar\" : [ 2, 3 ]" + + System.lineSeparator() // + + "}"); + } + + @Test + void decodes() throws Exception { + List zones = new LinkedList<>(); + zones.add(new Zone("denominator.io.")); + zones.add(new Zone("denominator.io.", "ABCD")); + + Response response = + Response.builder() + .status(200) + .reason("OK") + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .headers(Collections.emptyMap()) + .body(zonesJson, UTF_8) + .build(); + assertThat(new Jackson3Decoder().decode(response, new TypeReference>() {}.getType())) + .isEqualTo(zones); + } + + @Test + void nullBodyDecodesToNull() throws Exception { + Response response = + Response.builder() + .status(204) + .reason("OK") + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .headers(Collections.emptyMap()) + .build(); + assertThat(new Jackson3Decoder().decode(response, String.class)).isNull(); + } + + @Test + void emptyBodyDecodesToNull() throws Exception { + Response response = + Response.builder() + .status(204) + .reason("OK") + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .headers(Collections.emptyMap()) + .body(new byte[0]) + .build(); + assertThat(new Jackson3Decoder().decode(response, String.class)).isNull(); + } + + @Test + void customDecoder() throws Exception { + Jackson3Decoder decoder = + new Jackson3Decoder( + Arrays.asList(new SimpleModule().addDeserializer(Zone.class, new ZoneDeserializer()))); + + List zones = new LinkedList<>(); + zones.add(new Zone("DENOMINATOR.IO.")); + zones.add(new Zone("DENOMINATOR.IO.", "ABCD")); + + Response response = + Response.builder() + .status(200) + .reason("OK") + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .headers(Collections.emptyMap()) + .body(zonesJson, UTF_8) + .build(); + assertThat(decoder.decode(response, new TypeReference>() {}.getType())) + .isEqualTo(zones); + } + + @Test + void customEncoder() { + Jackson3Encoder encoder = + new Jackson3Encoder( + Arrays.asList(new SimpleModule().addSerializer(Zone.class, new ZoneSerializer()))); + + List zones = new LinkedList<>(); + zones.add(new Zone("denominator.io.")); + zones.add(new Zone("denominator.io.", "abcd")); + + RequestTemplate template = new RequestTemplate(); + encoder.encode(zones, new TypeReference>() {}.getType(), template); + + assertThat(template) + .hasBody( + "" // + + "[ {" + + System.lineSeparator() + + " \"name\" : \"DENOMINATOR.IO.\"" + + System.lineSeparator() + + "}, {" + + System.lineSeparator() + + " \"name\" : \"DENOMINATOR.IO.\"," + + System.lineSeparator() + + " \"id\" : \"ABCD\"" + + System.lineSeparator() + + "} ]"); + } + + @Test + void decoderCharset() throws IOException { + Zone zone = new Zone("denominator.io.", "ÁÉÍÓÚÀÈÌÒÙÄËÏÖÜÑ"); + + Map> headers = new HashMap<>(); + headers.put("Content-Type", Arrays.asList("application/json;charset=ISO-8859-1")); + + Response response = + Response.builder() + .status(200) + .reason("OK") + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .headers(headers) + .body( + new String( + "" // + + "{" + + System.lineSeparator() + + " \"name\" : \"DENOMINATOR.IO.\"," + + System.lineSeparator() + + " \"id\" : \"ÁÉÍÓÚÀÈÌÒÙÄËÏÖÜÑ\"" + + System.lineSeparator() + + "}") + .getBytes(StandardCharsets.ISO_8859_1)) + .build(); + assertThat( + ((Zone) new Jackson3Decoder().decode(response, new TypeReference() {}.getType()))) + .containsEntry("id", zone.get("id")); + } + + @Test + void decodesIterator() throws Exception { + List zones = new LinkedList<>(); + zones.add(new Zone("denominator.io.")); + zones.add(new Zone("denominator.io.", "ABCD")); + + Response response = + Response.builder() + .status(200) + .reason("OK") + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .headers(Collections.emptyMap()) + .body(zonesJson, UTF_8) + .build(); + Object decoded = + Jackson3IteratorDecoder.create() + .decode(response, new TypeReference>() {}.getType()); + assertThat(Iterator.class.isAssignableFrom(decoded.getClass())).isTrue(); + assertThat(Closeable.class.isAssignableFrom(decoded.getClass())).isTrue(); + assertThat(asList((Iterator) decoded)).isEqualTo(zones); + } + + private List asList(Iterator iter) { + final List copy = new ArrayList<>(); + while (iter.hasNext()) { + copy.add(iter.next()); + } + return copy; + } + + @Test + void nullBodyDecodesToEmptyIterator() throws Exception { + Response response = + Response.builder() + .status(204) + .reason("OK") + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .headers(Collections.emptyMap()) + .build(); + assertThat((byte[]) Jackson3IteratorDecoder.create().decode(response, byte[].class)).isEmpty(); + } + + @Test + void emptyBodyDecodesToEmptyIterator() throws Exception { + Response response = + Response.builder() + .status(204) + .reason("OK") + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .headers(Collections.emptyMap()) + .body(new byte[0]) + .build(); + assertThat((byte[]) Jackson3IteratorDecoder.create().decode(response, byte[].class)).isEmpty(); + } + + static class Zone extends LinkedHashMap { + + private static final long serialVersionUID = 1L; + + Zone() { + // for reflective instantiation. + } + + Zone(String name) { + this(name, null); + } + + Zone(String name, String id) { + put("name", name); + if (id != null) { + put("id", id); + } + } + } + + static class ZoneDeserializer extends StdDeserializer { + + public ZoneDeserializer() { + super(Zone.class); + } + + @Override + public Zone deserialize(JsonParser jp, DeserializationContext ctxt) { + Zone zone = new Zone(); + jp.nextToken(); + while (jp.nextToken() != JsonToken.END_OBJECT) { + String name = jp.currentName(); + String value = jp.getValueAsString(); + if (value != null) { + zone.put(name, value.toUpperCase()); + } + } + return zone; + } + } + + static class ZoneSerializer extends StdSerializer { + + public ZoneSerializer() { + super(Zone.class); + } + + @Override + public void serialize(Zone value, JsonGenerator jgen, SerializationContext provider) + throws JacksonException { + jgen.writeStartObject(); + for (Map.Entry entry : value.entrySet()) { + jgen.writeName(entry.getKey()); + jgen.writeString(entry.getValue().toString().toUpperCase()); + } + jgen.writeEndObject(); + } + } + + /** Enabled via {@link feign.Feign.Builder#dismiss404()} */ + @Test + void notFoundDecodesToEmpty() throws Exception { + Response response = + Response.builder() + .status(404) + .reason("NOT FOUND") + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .headers(Collections.emptyMap()) + .build(); + assertThat((byte[]) new Jackson3Decoder().decode(response, byte[].class)).isEmpty(); + } + + /** Enabled via {@link feign.Feign.Builder#dismiss404()} */ + @Test + void notFoundDecodesToEmptyIterator() throws Exception { + Response response = + Response.builder() + .status(404) + .reason("NOT FOUND") + .request( + Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) + .headers(Collections.emptyMap()) + .build(); + assertThat((byte[]) Jackson3IteratorDecoder.create().decode(response, byte[].class)).isEmpty(); + } +} diff --git a/jackson3/src/test/java/feign/jackson3/examples/GitHubExample.java b/jackson3/src/test/java/feign/jackson3/examples/GitHubExample.java new file mode 100644 index 0000000000..4bced86a97 --- /dev/null +++ b/jackson3/src/test/java/feign/jackson3/examples/GitHubExample.java @@ -0,0 +1,59 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * 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.jackson3.examples; + +import feign.Feign; +import feign.Param; +import feign.RequestLine; +import feign.jackson3.Jackson3Decoder; +import java.util.List; + +/** adapted from {@code com.example.retrofit.GitHubClient} */ +public class GitHubExample { + + public static void main(String... args) { + GitHub github = + Feign.builder() + .decoder(new Jackson3Decoder()) + .target(GitHub.class, "https://api.github.com"); + + System.out.println("Let's fetch and print a list of the contributors to this library."); + List contributors = github.contributors("netflix", "feign"); + for (Contributor contributor : contributors) { + System.out.println(contributor.login + " (" + contributor.contributions + ")"); + } + } + + interface GitHub { + + @RequestLine("GET /repos/{owner}/{repo}/contributors") + List contributors(@Param("owner") String owner, @Param("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; + } + } +} diff --git a/jackson3/src/test/java/feign/jackson3/examples/GitHubIteratorExample.java b/jackson3/src/test/java/feign/jackson3/examples/GitHubIteratorExample.java new file mode 100644 index 0000000000..9bca014240 --- /dev/null +++ b/jackson3/src/test/java/feign/jackson3/examples/GitHubIteratorExample.java @@ -0,0 +1,67 @@ +/* + * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) + * + * 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.jackson3.examples; + +import feign.Feign; +import feign.Param; +import feign.RequestLine; +import feign.jackson3.Jackson3IteratorDecoder; +import java.io.Closeable; +import java.io.IOException; +import java.util.Iterator; + +/** adapted from {@code com.example.retrofit.GitHubClient} */ +public class GitHubIteratorExample { + + public static void main(String... args) throws IOException { + GitHub github = + Feign.builder() + .decoder(Jackson3IteratorDecoder.create()) + .doNotCloseAfterDecode() + .target(GitHub.class, "https://api.github.com"); + + System.out.println("Let's fetch and print a list of the contributors to this library."); + Iterator contributors = github.contributors("OpenFeign", "feign"); + try { + while (contributors.hasNext()) { + Contributor contributor = contributors.next(); + System.out.println(contributor.login + " (" + contributor.contributions + ")"); + } + } finally { + ((Closeable) contributors).close(); + } + } + + interface GitHub { + + @RequestLine("GET /repos/{owner}/{repo}/contributors") + Iterator contributors(@Param("owner") String owner, @Param("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; + } + } +} diff --git a/pom.xml b/pom.xml index 9df6ff2076..1e392888f3 100644 --- a/pom.xml +++ b/pom.xml @@ -89,6 +89,7 @@ hc5 hystrix jackson + jackson3 jackson-jaxb jackson-jr jaxb @@ -168,7 +169,7 @@ 6.0.2 2.21.0 - 2.21 + 3.0.2 3.27.7 5.21.0 2.0.60.android8 @@ -470,33 +471,11 @@ - com.fasterxml.jackson.core - jackson-databind - ${jackson.version} - - - - com.fasterxml.jackson.core - jackson-core - ${jackson.version} - - - - com.fasterxml.jackson.core - jackson-annotations - ${jackson.annotations.version} - - - - com.fasterxml.jackson.jr - jackson-jr-objects - ${jackson.version} - - - - com.fasterxml.jackson.jr - jackson-jr-annotation-support + com.fasterxml.jackson + jackson-bom ${jackson.version} + pom + import