diff --git a/core/src/main/java/feign/Util.java b/core/src/main/java/feign/Util.java index 5393a5569..8ecb19970 100644 --- a/core/src/main/java/feign/Util.java +++ b/core/src/main/java/feign/Util.java @@ -69,6 +69,9 @@ public class Util { /** The HTTP Accept-Encoding header field name. */ public static final String ACCEPT_ENCODING = "Accept-Encoding"; + /** The HTTP Accept header field name. */ + public static final String ACCEPT = "Accept"; + /** The HTTP Retry-After header field name. */ public static final String RETRY_AFTER = "Retry-After"; diff --git a/core/src/main/java/feign/codec/DefaultDecoder.java b/core/src/main/java/feign/codec/DefaultDecoder.java index c6ada1025..04bcf1f12 100644 --- a/core/src/main/java/feign/codec/DefaultDecoder.java +++ b/core/src/main/java/feign/codec/DefaultDecoder.java @@ -22,6 +22,19 @@ public class DefaultDecoder extends StringDecoder { + /** + * Accepts exactly what {@link #decode} handles: everything {@link StringDecoder} accepts, plus a + * {@code byte[]} return type. + * + * @param response {@inheritDoc} + * @param type {@inheritDoc} + * @return {@inheritDoc} + */ + @Override + public boolean canDecode(Response response, Type type) { + return byte[].class.equals(type) || super.canDecode(response, type); + } + @Override public Object decode(Response response, Type type) throws IOException { if (response.status() == 404 || response.status() == 204) return Util.emptyValueOf(type); diff --git a/core/src/main/java/feign/codec/DefaultEncoder.java b/core/src/main/java/feign/codec/DefaultEncoder.java index 39a0f8dab..bfa8efbdf 100644 --- a/core/src/main/java/feign/codec/DefaultEncoder.java +++ b/core/src/main/java/feign/codec/DefaultEncoder.java @@ -20,7 +20,21 @@ import feign.RequestTemplate; import java.lang.reflect.Type; -public class DefaultEncoder implements Encoder { +public class DefaultEncoder implements PredicatedEncoder { + + /** + * Accepts exactly what {@link #encode} handles: a {@code String} or {@code byte[]} body, and a + * null body, which is sent as no body at all. + * + * @param object {@inheritDoc} + * @param bodyType {@inheritDoc} + * @param template {@inheritDoc} + * @return {@inheritDoc} + */ + @Override + public boolean canEncode(Object object, Type bodyType, RequestTemplate template) { + return bodyType == String.class || bodyType == byte[].class || object == null; + } @Override public void encode(Object object, Type bodyType, RequestTemplate template) { diff --git a/core/src/main/java/feign/codec/MultiDecoder.java b/core/src/main/java/feign/codec/MultiDecoder.java index f48ee1975..47e54e1d4 100644 --- a/core/src/main/java/feign/codec/MultiDecoder.java +++ b/core/src/main/java/feign/codec/MultiDecoder.java @@ -17,6 +17,7 @@ import feign.Experimental; import feign.FeignException; +import feign.Request; import feign.Response; import feign.Util; import java.io.IOException; @@ -52,11 +53,20 @@ * naming what was tried. Add a decoder guarded by {@link DecoderPredicate#any()} last to act as a * default, as above. * + *
A multi-decoder is itself a {@link PredicatedDecoder}, accepting whatever any of its decoders + * accepts, so one can be added to another. That is how a library ships a set of decoders as a + * single unit: given a hypothetical {@code AcmeFeign.decoders()} returning a multi-decoder over + * that library's decoders, the whole set is added in one go: + * + *
+ * Feign.builder().decoders(AcmeFeign.decoders(), new JacksonDecoder()); + *+ * * @see PredicatedDecoder * @see DecoderPredicate */ @Experimental -public class MultiDecoder implements Decoder { +public class MultiDecoder implements PredicatedDecoder { private final List
+ * MultiDecoder.builder() + * .narrow(DecoderPredicate.status(200), new JacksonDecoder()) + * .add(DecoderPredicate.any(), new DefaultDecoder()) + * .build(); + *+ * + * @param predicate narrows what the decoder handles + * @param decoder the decoder to delegate to + */ + public Builder narrow(DecoderPredicate predicate, Decoder decoder) { + return add(PredicatedDecoder.narrowing(predicate, decoder)); + } + + /** + * Builds the multi-decoder. + * + * @throws IllegalStateException if no decoder was added + */ public MultiDecoder build() { + if (decoders.isEmpty()) { + throw new IllegalStateException("at least one decoder is required"); + } return new MultiDecoder(decoders); } } diff --git a/core/src/main/java/feign/codec/MultiEncoder.java b/core/src/main/java/feign/codec/MultiEncoder.java index 23feaf35b..b17b615ed 100644 --- a/core/src/main/java/feign/codec/MultiEncoder.java +++ b/core/src/main/java/feign/codec/MultiEncoder.java @@ -50,11 +50,20 @@ * naming what was tried. Add an encoder guarded by {@link EncoderPredicate#any()} last to act as a * default, as above. * + *
A multi-encoder is itself a {@link PredicatedEncoder}, accepting whatever any of its encoders + * accepts, so one can be added to another. That is how a library ships a set of encoders as a + * single unit: given a hypothetical {@code AcmeFeign.encoders()} returning a multi-encoder over + * that library's encoders, the whole set is added in one go: + * + *
+ * Feign.builder().encoders(AcmeFeign.encoders(), new JacksonEncoder()); + *+ * * @see PredicatedEncoder * @see EncoderPredicate */ @Experimental -public class MultiEncoder implements Encoder { +public class MultiEncoder implements PredicatedEncoder { private final List
+ * MultiEncoder.builder()
+ * .narrow(EncoderPredicate.contentType("application/vnd.acme+json"), new GsonEncoder())
+ * .add(EncoderPredicate.any(), new DefaultEncoder())
+ * .build();
+ *
+ *
+ * @param predicate narrows what the encoder handles
+ * @param encoder the encoder to delegate to
+ */
+ public Builder narrow(EncoderPredicate predicate, Encoder encoder) {
+ return add(PredicatedEncoder.narrowing(predicate, encoder));
+ }
+
+ /**
+ * Builds the multi-encoder.
+ *
+ * @throws IllegalStateException if no encoder was added
+ */
public MultiEncoder build() {
+ if (encoders.isEmpty()) {
+ throw new IllegalStateException("at least one encoder is required");
+ }
return new MultiEncoder(encoders);
}
}
diff --git a/core/src/main/java/feign/codec/PredicatedDecoder.java b/core/src/main/java/feign/codec/PredicatedDecoder.java
index d8d48467a..9b3de742c 100644
--- a/core/src/main/java/feign/codec/PredicatedDecoder.java
+++ b/core/src/main/java/feign/codec/PredicatedDecoder.java
@@ -71,6 +71,9 @@ public interface PredicatedDecoder extends Decoder {
* PredicatedDecoder.of(DecoderPredicate.any(), new DefaultDecoder()));
*
*
+ * The predicate is used instead of the decoder's own, not in addition to it. {@link + * MultiDecoder.Builder#add(DecoderPredicate, Decoder)} is the same thing at the call site. + * * @param predicate decides whether the decoder handles a response * @param decoder the decoder to delegate to */ @@ -90,6 +93,9 @@ static PredicatedDecoder of(DecoderPredicate predicate, Decoder decoder) { *
A decoder that does not implement {@link PredicatedDecoder} declares nothing to narrow, so * this behaves like {@link #of(DecoderPredicate, Decoder)}. * + *
The predicate is used in addition to the decoder's own, not instead of it. {@link + * MultiDecoder.Builder#narrow(DecoderPredicate, Decoder)} is the same thing at the call site. + * * @param predicate narrows what the decoder handles * @param decoder the decoder to delegate to */ diff --git a/core/src/main/java/feign/codec/PredicatedEncoder.java b/core/src/main/java/feign/codec/PredicatedEncoder.java index f9cd135ac..56cc30c87 100644 --- a/core/src/main/java/feign/codec/PredicatedEncoder.java +++ b/core/src/main/java/feign/codec/PredicatedEncoder.java @@ -71,6 +71,9 @@ public interface PredicatedEncoder extends Encoder { * PredicatedEncoder.of(EncoderPredicate.any(), new Encoder.Default())); * * + *
The predicate is used instead of the encoder's own, not in addition to it. {@link + * MultiEncoder.Builder#add(EncoderPredicate, Encoder)} is the same thing at the call site. + * * @param predicate decides whether the encoder handles a request * @param encoder the encoder to delegate to */ @@ -90,6 +93,9 @@ static PredicatedEncoder of(EncoderPredicate predicate, Encoder encoder) { *
An encoder that does not implement {@link PredicatedEncoder} declares nothing to narrow, so * this behaves like {@link #of(EncoderPredicate, Encoder)}. * + *
The predicate is used in addition to the encoder's own, not instead of it. {@link
+ * MultiEncoder.Builder#narrow(EncoderPredicate, Encoder)} is the same thing at the call site.
+ *
* @param predicate narrows what the encoder handles
* @param encoder the encoder to delegate to
*/
diff --git a/core/src/main/java/feign/codec/StringDecoder.java b/core/src/main/java/feign/codec/StringDecoder.java
index 0982110b4..e88649b8a 100644
--- a/core/src/main/java/feign/codec/StringDecoder.java
+++ b/core/src/main/java/feign/codec/StringDecoder.java
@@ -22,7 +22,23 @@
import java.io.IOException;
import java.lang.reflect.Type;
-public class StringDecoder implements Decoder {
+public class StringDecoder implements PredicatedDecoder {
+
+ /**
+ * Accepts exactly what {@link #decode} handles: a {@code String} return type, and any type at all
+ * when there is no body to read.
+ *
+ * @param response {@inheritDoc}
+ * @param type {@inheritDoc}
+ * @return {@inheritDoc}
+ */
+ @Override
+ public boolean canDecode(Response response, Type type) {
+ return response.status() == 404
+ || response.status() == 204
+ || response.body() == null
+ || String.class.equals(type);
+ }
@Override
public Object decode(Response response, Type type) throws IOException {
diff --git a/core/src/test/java/feign/codec/DefaultDecoderTest.java b/core/src/test/java/feign/codec/DefaultDecoderTest.java
index a88ff4dd8..d2bc51568 100644
--- a/core/src/test/java/feign/codec/DefaultDecoderTest.java
+++ b/core/src/test/java/feign/codec/DefaultDecoderTest.java
@@ -36,7 +36,15 @@
@SuppressWarnings("deprecation")
class DefaultDecoderTest {
- private final Decoder decoder = new DefaultDecoder();
+ private final DefaultDecoder decoder = new DefaultDecoder();
+
+ @Test
+ void declaresTheTypesItDecodes() throws Exception {
+ assertThat(decoder.canDecode(knownResponse(), String.class)).isTrue();
+ assertThat(decoder.canDecode(knownResponse(), byte[].class)).isTrue();
+ assertThat(decoder.canDecode(knownResponse(), Document.class)).isFalse();
+ assertThat(decoder.canDecode(nullBodyResponse(), Document.class)).isTrue();
+ }
@Test
void decodesToString() throws Exception {
diff --git a/core/src/test/java/feign/codec/DefaultEncoderTest.java b/core/src/test/java/feign/codec/DefaultEncoderTest.java
index 9aecbb905..0770161cb 100644
--- a/core/src/test/java/feign/codec/DefaultEncoderTest.java
+++ b/core/src/test/java/feign/codec/DefaultEncoderTest.java
@@ -26,7 +26,16 @@
class DefaultEncoderTest {
- private final Encoder encoder = new DefaultEncoder();
+ private final DefaultEncoder encoder = new DefaultEncoder();
+
+ @Test
+ void declaresTheTypesItEncodes() {
+ RequestTemplate template = new RequestTemplate();
+ assertThat(encoder.canEncode("content", String.class, template)).isTrue();
+ assertThat(encoder.canEncode(new byte[0], byte[].class, template)).isTrue();
+ assertThat(encoder.canEncode(null, Clock.class, template)).isTrue();
+ assertThat(encoder.canEncode(Clock.systemUTC(), Clock.class, template)).isFalse();
+ }
@Test
void encodesStrings() throws Exception {
diff --git a/core/src/test/java/feign/codec/MultiDecoderTest.java b/core/src/test/java/feign/codec/MultiDecoderTest.java
index 5ca7502b1..5bc1aebbe 100644
--- a/core/src/test/java/feign/codec/MultiDecoderTest.java
+++ b/core/src/test/java/feign/codec/MultiDecoderTest.java
@@ -242,7 +242,8 @@ void throwsWhenNoDecoderAcceptsTheResponse() {
+ " Decoders tried, in order:"
+ "\n - SelfDeclaringJsonDecoder"
+ "\n - RecordingDecoder when Content-Type is XML"
- + "\nAdd a decoder guarded by DecoderPredicate.any() last to act as a default.");
+ + "\nRegister a decoder that accepts it, or add a catch-all"
+ + " (DecoderPredicate.any()) last.");
}
@Test
@@ -256,14 +257,9 @@ void theFailureReportsAMissingContentType() {
@Test
void throwsWhenNoDecodersAreConfigured() {
- Decoder decoder = MultiDecoder.builder().build();
-
- assertThatThrownBy(
- () -> decoder.decode(responseWithContentType("application/json"), String.class))
- .isInstanceOf(DecodeException.class)
- .hasMessage(
- "Unable to decode 200 response (Content-Type: application/json) as java.lang.String."
- + " No decoders were configured.");
+ assertThatThrownBy(() -> MultiDecoder.builder().build())
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessage("at least one decoder is required");
}
@Test
@@ -297,6 +293,98 @@ void rejectsNullDecoders() {
.hasMessage("decoder cannot be null");
}
+ @Test
+ void aMultiDecoderNestsInsideAnother() throws IOException {
+ RecordingDecoder xml = new RecordingDecoder("xml");
+ PredicatedDecoder contributed =
+ MultiDecoder.builder()
+ .add(new SelfDeclaringJsonDecoder())
+ .add(DecoderPredicate.xmlContentType(), xml)
+ .build();
+
+ Decoder decoder =
+ MultiDecoder.builder()
+ .add(contributed)
+ .add(DecoderPredicate.any(), new RecordingDecoder("fallback"))
+ .build();
+
+ assertThat(decoder.decode(responseWithContentType("application/xml"), String.class))
+ .isEqualTo("xml");
+ assertThat(xml.invoked).isTrue();
+ }
+
+ @Test
+ void aNestedMultiDecoderAcceptsWhateverItsDecodersAccept() {
+ PredicatedDecoder contributed =
+ MultiDecoder.builder().add(new SelfDeclaringJsonDecoder()).build();
+
+ assertThat(contributed.canDecode(responseWithContentType("application/json"), String.class))
+ .isTrue();
+ assertThat(contributed.canDecode(responseWithContentType("text/plain"), String.class))
+ .isFalse();
+ }
+
+ @Test
+ void theFailureUnfoldsNestedDecoders() {
+ PredicatedDecoder contributed =
+ MultiDecoder.builder()
+ .add(new SelfDeclaringJsonDecoder())
+ .add(DecoderPredicate.xmlContentType(), new RecordingDecoder("xml"))
+ .build();
+
+ Decoder decoder =
+ MultiDecoder.builder().add(new SelfDeclaringJsonDecoder()).add(contributed).build();
+
+ assertThatThrownBy(() -> decoder.decode(responseWithContentType("text/plain"), String.class))
+ .isInstanceOf(DecodeException.class)
+ .hasMessageContaining(
+ "Decoders tried, in order:"
+ + "\n - SelfDeclaringJsonDecoder"
+ + "\n - MultiDecoder:"
+ + "\n - SelfDeclaringJsonDecoder"
+ + "\n - RecordingDecoder when Content-Type is XML");
+ }
+
+ @Test
+ void theFailureReportsTheRequestedAcceptHeader() {
+ Map