Browse Source

Support binary request/response bodies (#57)

Request/Response/RequestTemplate are now fundamentally based on a byte[] body field.
For Request/RequestTemplate, if a charset is provided, it can be treated as text.
For many users of the library, the change should barely be noticeable, as the methods that
were changed were mostly used internally.
There were some non-backwards-compatible signature changes that require a
major version bump, however.
pull/83/head
David M. Carr 11 years ago
parent
commit
91e4d8209a
  1. 3
      CHANGES.md
  2. 17
      README.md
  3. 8
      core/src/main/java/feign/Client.java
  4. 38
      core/src/main/java/feign/Logger.java
  5. 5
      core/src/main/java/feign/MethodHandler.java
  6. 30
      core/src/main/java/feign/Request.java
  7. 50
      core/src/main/java/feign/RequestTemplate.java
  8. 92
      core/src/main/java/feign/Response.java
  9. 51
      core/src/main/java/feign/Util.java
  10. 12
      core/src/main/java/feign/codec/Decoder.java
  11. 4
      core/src/main/java/feign/codec/Encoder.java
  12. 5
      core/src/test/java/feign/DefaultContractTest.java
  13. 37
      core/src/test/java/feign/FeignTest.java
  14. 16
      core/src/test/java/feign/codec/DefaultDecoderTest.java
  15. 9
      core/src/test/java/feign/codec/DefaultEncoderTest.java
  16. 3
      core/src/test/java/feign/codec/DefaultErrorDecoderTest.java
  17. 35
      gson/src/test/java/feign/gson/GsonModuleTest.java
  18. 6
      jackson/src/main/java/feign/jackson/JacksonDecoder.java
  19. 12
      jackson/src/test/java/feign/jackson/JacksonModuleTest.java
  20. 3
      ribbon/src/main/java/feign/ribbon/LBClient.java
  21. 8
      sax/src/main/java/feign/sax/SAXDecoder.java
  22. 4
      sax/src/test/java/feign/sax/SAXDecoderTest.java
  23. 7
      sax/src/test/java/feign/sax/examples/AWSSignatureVersion4.java

3
CHANGES.md

@ -1,3 +1,6 @@ @@ -1,3 +1,6 @@
### Version 6.0
* Support binary request and response bodies.
### Version 5.4.0
* Add `BasicAuthRequestInterceptor`
* Add Jackson integration

17
README.md

@ -144,9 +144,9 @@ MyService api = Feign.create(MyService.class, "https://myAppProd", new RibbonMod @@ -144,9 +144,9 @@ MyService api = Feign.create(MyService.class, "https://myAppProd", new RibbonMod
### Decoders
`Feign.builder()` allows you to specify additional configuration such as how to decode a response.
If any methods in your interface return types besides `Response`, `String` or `void`, you'll need to configure a `Decoder`.
If any methods in your interface return types besides `Response`, `String`, `byte[]` or `void`, you'll need to configure a non-default `Decoder`.
Here's how to configure json decoding (using the `feign-gson` extension):
Here's how to configure JSON decoding (using the `feign-gson` extension):
```java
GitHub github = Feign.builder()
@ -154,6 +154,19 @@ GitHub github = Feign.builder() @@ -154,6 +154,19 @@ GitHub github = Feign.builder()
.target(GitHub.class, "https://api.github.com");
```
### Encoders
`Feign.builder()` allows you to specify additional configuration such as how to encode a request.
If any methods in your interface use parameters types besides `String` or `byte[]`, you'll need to configure a non-default `Encoder`.
Here's how to configure JSON encoding (using the `feign-gson` extension):
```json
GitHub github = Feign.builder()
.encoder(new GsonEncoder())
.target(GitHub.class, "https://api.github.com");
```
### Advanced usage and Dagger
#### Dagger
Feign can be directly wired into Dagger which keeps things at compile time and Android friendly. As opposed to exposing builders for config, Feign intends users to embed their config in Dagger.

8
core/src/main/java/feign/Client.java

@ -17,9 +17,7 @@ package feign; @@ -17,9 +17,7 @@ package feign;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Collection;
@ -39,7 +37,6 @@ import feign.Request.Options; @@ -39,7 +37,6 @@ import feign.Request.Options;
import static feign.Util.CONTENT_ENCODING;
import static feign.Util.CONTENT_LENGTH;
import static feign.Util.ENCODING_GZIP;
import static feign.Util.UTF_8;
/**
* Submits HTTP {@link Request requests}. Implementations are expected to be
@ -113,7 +110,7 @@ public interface Client { @@ -113,7 +110,7 @@ public interface Client {
out = new GZIPOutputStream(out);
}
try {
out.write(request.body().getBytes(UTF_8));
out.write(request.body());
} finally {
try {
out.close();
@ -144,8 +141,7 @@ public interface Client { @@ -144,8 +141,7 @@ public interface Client {
} else {
stream = connection.getInputStream();
}
Reader body = stream != null ? new InputStreamReader(stream, UTF_8) : null;
return Response.create(status, reason, headers, body, length);
return Response.create(status, reason, headers, stream, length);
}
}
}

38
core/src/main/java/feign/Logger.java

@ -15,17 +15,15 @@ @@ -15,17 +15,15 @@
*/
package feign;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.util.logging.FileHandler;
import java.util.logging.LogRecord;
import java.util.logging.SimpleFormatter;
import static feign.Util.decodeOrDefault;
import static feign.Util.UTF_8;
import static feign.Util.ensureClosed;
import static feign.Util.valuesOrEmpty;
/**
@ -145,15 +143,16 @@ public abstract class Logger { @@ -145,15 +143,16 @@ public abstract class Logger {
}
}
int bytes = 0;
int bodyLength = 0;
if (request.body() != null) {
bytes = request.body().getBytes(UTF_8).length;
bodyLength = request.body().length;
if (logLevel.ordinal() >= Level.FULL.ordinal()) {
String bodyText = request.charset() != null ? new String(request.body(), request.charset()) : null;
log(configKey, ""); // CRLF
log(configKey, "%s", request.body());
log(configKey, "%s", bodyText != null ? bodyText : "Binary data");
}
}
log(configKey, "---> END HTTP (%s-byte body)", bytes);
log(configKey, "---> END HTTP (%s-byte body)", bodyLength);
}
}
@ -171,27 +170,20 @@ public abstract class Logger { @@ -171,27 +170,20 @@ public abstract class Logger {
}
}
int bodyLength = 0;
if (response.body() != null) {
if (logLevel.ordinal() >= Level.FULL.ordinal()) {
log(configKey, ""); // CRLF
}
BufferedReader reader = new BufferedReader(response.body().asReader());
try {
StringBuilder buffered = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
buffered.append(line);
if (logLevel.ordinal() >= Level.FULL.ordinal()) {
log(configKey, "%s", line);
}
}
String bodyAsString = buffered.toString();
log(configKey, "<--- END HTTP (%s-byte body)", bodyAsString.getBytes(UTF_8).length);
return Response.create(response.status(), response.reason(), response.headers(), bodyAsString);
} finally {
ensureClosed(reader);
byte[] bodyData = Util.toByteArray(response.body().asInputStream());
bodyLength = bodyData.length;
if (logLevel.ordinal() >= Level.FULL.ordinal() && bodyLength > 0) {
log(configKey, "%s", decodeOrDefault(bodyData, UTF_8, "Binary data"));
}
log(configKey, "<--- END HTTP (%s-byte body)", bodyLength);
return Response.create(response.status(), response.reason(), response.headers(), bodyData);
} else {
log(configKey, "<--- END HTTP (%s-byte body)", bodyLength);
}
}
return response;

5
core/src/main/java/feign/MethodHandler.java

@ -141,8 +141,9 @@ interface MethodHandler { @@ -141,8 +141,9 @@ interface MethodHandler {
if (response.body() == null) {
return response;
}
String bodyString = Util.toString(response.body().asReader());
return Response.create(response.status(), response.reason(), response.headers(), bodyString);
// Ensure the response body is disconnected
byte[] bodyData = Util.toByteArray(response.body().asInputStream());
return Response.create(response.status(), response.reason(), response.headers(), bodyData);
} else if (void.class == metadata.returnType()) {
return null;
} else {

30
core/src/main/java/feign/Request.java

@ -15,6 +15,7 @@ @@ -15,6 +15,7 @@
*/
package feign;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
@ -25,26 +26,23 @@ import static feign.Util.valuesOrEmpty; @@ -25,26 +26,23 @@ import static feign.Util.valuesOrEmpty;
/**
* An immutable request to an http server.
* <br>
* <br><br><b>Note</b><br>
* <br>
* Since {@link Feign} is designed for non-binary apis, and expectations are
* that any request can be replayed, we only support a String body.
*/
public final class Request {
private final String method;
private final String url;
private final Map<String, Collection<String>> headers;
private final String body;
private final byte[] body;
private final Charset charset;
Request(String method, String url, Map<String, Collection<String>> headers, String body) {
Request(String method, String url, Map<String, Collection<String>> headers, byte[] body, Charset charset) {
this.method = checkNotNull(method, "method of %s", url);
this.url = checkNotNull(url, "url");
LinkedHashMap<String, Collection<String>> copyOf = new LinkedHashMap<String, Collection<String>>();
copyOf.putAll(checkNotNull(headers, "headers of %s %s", method, url));
this.headers = Collections.unmodifiableMap(copyOf);
this.body = body; // nullable
this.charset = charset; // nullable
}
/* Method to invoke on the server. */
@ -62,8 +60,20 @@ public final class Request { @@ -62,8 +60,20 @@ public final class Request {
return headers;
}
/* If present, this is the replayable body to send to the server. */
public String body() {
/**
* The character set with which the body is encoded, or null if unknown or not applicable. When this is
* present, you can use {@code new String(req.body(), req.charset())} to access the body as a String.
*/
public Charset charset() {
return charset;
}
/**
* If present, this is the replayable body to send to the server. In some cases, this may be interpretable as text.
*
* @see #charset()
*/
public byte[] body() {
return body;
}
@ -110,7 +120,7 @@ public final class Request { @@ -110,7 +120,7 @@ public final class Request {
}
}
if (body != null) {
builder.append('\n').append(body);
builder.append('\n').append(charset != null ? new String(body, charset) : "Binary data");
}
return builder.toString();
}

50
core/src/main/java/feign/RequestTemplate.java

@ -19,6 +19,7 @@ import java.io.Serializable; @@ -19,6 +19,7 @@ import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@ -54,7 +55,8 @@ public final class RequestTemplate implements Serializable { @@ -54,7 +55,8 @@ public final class RequestTemplate implements Serializable {
private StringBuilder url = new StringBuilder();
private final Map<String, Collection<String>> queries = new LinkedHashMap<String, Collection<String>>();
private final Map<String, Collection<String>> headers = new LinkedHashMap<String, Collection<String>>();
private String body;
private transient Charset charset;
private byte[] body;
private String bodyTemplate;
public RequestTemplate() {
@ -68,6 +70,7 @@ public final class RequestTemplate implements Serializable { @@ -68,6 +70,7 @@ public final class RequestTemplate implements Serializable {
this.url.append(toCopy.url);
this.queries.putAll(toCopy.queries);
this.headers.putAll(toCopy.headers);
this.charset = toCopy.charset;
this.body = toCopy.body;
this.bodyTemplate = toCopy.bodyTemplate;
}
@ -117,7 +120,7 @@ public final class RequestTemplate implements Serializable { @@ -117,7 +120,7 @@ public final class RequestTemplate implements Serializable {
/* roughly analogous to {@code javax.ws.rs.client.Target.request()}. */
public Request request() {
return new Request(method, new StringBuilder(url).append(queryLine()).toString(),
headers, body);
headers, body, charset);
}
private static String urlDecode(String arg) {
@ -391,18 +394,39 @@ public final class RequestTemplate implements Serializable { @@ -391,18 +394,39 @@ public final class RequestTemplate implements Serializable {
*
* @see Request#body()
*/
public RequestTemplate body(String body) {
this.body = body;
if (this.body != null) {
byte[] contentLength = body.getBytes(UTF_8);
header(CONTENT_LENGTH, String.valueOf(contentLength.length));
}
public RequestTemplate body(byte[] bodyData, Charset charset) {
this.bodyTemplate = null;
this.charset = charset;
this.body = bodyData;
int bodyLength = bodyData != null ? bodyData.length : 0;
header(CONTENT_LENGTH, String.valueOf(bodyLength));
return this;
}
/* @see Request#body() */
public String body() {
/**
* replaces the {@link feign.Util#CONTENT_LENGTH} header.
* <br>
* Usually populated by an {@link feign.codec.Encoder}.
*
* @see Request#body()
*/
public RequestTemplate body(String bodyText) {
byte[] bodyData = bodyText != null ? bodyText.getBytes(UTF_8) : null;
return body(bodyData, UTF_8);
}
/**
* The character set with which the body is encoded, or null if unknown or not applicable. When this is
* present, you can use {@code new String(req.body(), req.charset())} to access the body as a String.
*/
public Charset charset() {
return charset;
}
/**
* @see Request#body()
*/
public byte[] body() {
return body;
}
@ -413,6 +437,7 @@ public final class RequestTemplate implements Serializable { @@ -413,6 +437,7 @@ public final class RequestTemplate implements Serializable {
*/
public RequestTemplate bodyTemplate(String bodyTemplate) {
this.bodyTemplate = bodyTemplate;
this.charset = null;
this.body = null;
return this;
}
@ -426,10 +451,7 @@ public final class RequestTemplate implements Serializable { @@ -426,10 +451,7 @@ public final class RequestTemplate implements Serializable {
}
/**
* if there are any query params in the {@link #body()}, this will extract
* them out.
*
* @return
* if there are any query params in the URL, this will extract them out.
*/
private StringBuilder pullAnyQueriesOutOfUrl(StringBuilder url) {
// parse out queries

92
core/src/main/java/feign/Response.java

@ -15,16 +15,20 @@ @@ -15,16 +15,20 @@
*/
package feign;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import static feign.Util.UTF_8;
import static feign.Util.decodeOrDefault;
import static feign.Util.checkNotNull;
import static feign.Util.checkState;
import static feign.Util.valuesOrEmpty;
@ -40,12 +44,18 @@ public final class Response { @@ -40,12 +44,18 @@ public final class Response {
private final Body body;
public static Response create(int status, String reason, Map<String, Collection<String>> headers,
Reader chars, Integer length) {
return new Response(status, reason, headers, ReaderBody.orNull(chars, length));
InputStream inputStream, Integer length) {
return new Response(status, reason, headers, InputStreamBody.orNull(inputStream, length));
}
public static Response create(int status, String reason, Map<String, Collection<String>> headers, String chars) {
return new Response(status, reason, headers, StringBody.orNull(chars));
public static Response create(int status, String reason, Map<String, Collection<String>> headers,
byte[] data) {
return new Response(status, reason, headers, ByteArrayBody.orNull(data));
}
public static Response create(int status, String reason, Map<String, Collection<String>> headers,
String text, Charset charset) {
return new Response(status, reason, headers, ByteArrayBody.orNull(text, charset));
}
private Response(int status, String reason, Map<String, Collection<String>> headers, Body body) {
@ -94,28 +104,34 @@ public final class Response { @@ -94,28 +104,34 @@ public final class Response {
Integer length();
/**
* True if {@link #asReader()} can be called more than once.
* True if {@link #asInputStream()} and {@link #asReader()} can be called more than once.
*/
boolean isRepeatable();
/**
* It is the responsibility of the caller to close the stream.
*/
InputStream asInputStream() throws IOException;
/**
* It is the responsibility of the caller to close the stream.
*/
Reader asReader() throws IOException;
}
private static final class ReaderBody implements Response.Body {
private static Body orNull(Reader chars, Integer length) {
if (chars == null)
private static final class InputStreamBody implements Response.Body {
private static Body orNull(InputStream inputStream, Integer length) {
if (inputStream == null) {
return null;
return new ReaderBody(chars, length);
}
return new InputStreamBody(inputStream, length);
}
private final Reader chars;
private final InputStream inputStream;
private final Integer length;
private ReaderBody(Reader chars, Integer length) {
this.chars = chars;
private InputStreamBody(InputStream inputStream, Integer length) {
this.inputStream = inputStream;
this.length = length;
}
@ -127,50 +143,62 @@ public final class Response { @@ -127,50 +143,62 @@ public final class Response {
return false;
}
@Override public InputStream asInputStream() throws IOException {
return inputStream;
}
@Override public Reader asReader() throws IOException {
return chars;
return new InputStreamReader(inputStream, UTF_8);
}
@Override public void close() throws IOException {
chars.close();
inputStream.close();
}
}
private static final class StringBody implements Response.Body {
private static Body orNull(String chars) {
if (chars == null)
private static final class ByteArrayBody implements Response.Body {
private static Body orNull(byte[] data) {
if (data == null) {
return null;
return new StringBody(chars);
}
return new ByteArrayBody(data);
}
private final String chars;
public StringBody(String chars) {
this.chars = chars;
private static Body orNull(String text, Charset charset) {
if (text == null) {
return null;
}
checkNotNull(charset, "charset");
return new ByteArrayBody(text.getBytes(charset));
}
private volatile Integer length;
private final byte[] data;
public ByteArrayBody(byte[] data) {
this.data = data;
}
@Override public Integer length() {
if (length == null) {
length = chars.getBytes(UTF_8).length;
}
return length;
return data.length;
}
@Override public boolean isRepeatable() {
return true;
}
@Override public InputStream asInputStream() throws IOException {
return new ByteArrayInputStream(data);
}
@Override public Reader asReader() throws IOException {
return new StringReader(chars);
return new InputStreamReader(asInputStream(), UTF_8);
}
public String toString() {
return chars;
@Override public void close() throws IOException {
}
@Override public void close() {
@Override public String toString() {
return decodeOrDefault(data, UTF_8, "Binary data");
}
}

51
core/src/main/java/feign/Util.java

@ -15,14 +15,19 @@ @@ -15,14 +15,19 @@
*/
package feign;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.lang.reflect.Array;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
@ -192,4 +197,50 @@ public class Util { @@ -192,4 +197,50 @@ public class Util {
ensureClosed(reader);
}
}
/**
* Adapted from {@code com.google.common.io.ByteStreams.toByteArray()}.
*/
public static byte[] toByteArray(InputStream in) throws IOException {
checkNotNull(in, "in");
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
copy(in, out);
return out.toByteArray();
} finally {
ensureClosed(in);
}
}
/**
* Adapted from {@code com.google.common.io.ByteStreams.copy()}.
*/
private static long copy(InputStream from, OutputStream to)
throws IOException {
checkNotNull(from, "from");
checkNotNull(to, "to");
byte[] buf = new byte[BUF_SIZE];
long total = 0;
while (true) {
int r = from.read(buf);
if (r == -1) {
break;
}
to.write(buf, 0, r);
total += r;
}
return total;
}
static String decodeOrDefault(byte[] data, Charset charset, String defaultValue) {
if (data == null) {
return defaultValue;
}
checkNotNull(charset, "charset");
try {
return charset.newDecoder().decode(ByteBuffer.wrap(data)).toString();
} catch (CharacterCodingException ex) {
return defaultValue;
}
}
}

12
core/src/main/java/feign/codec/Decoder.java

@ -17,6 +17,7 @@ package feign.codec; @@ -17,6 +17,7 @@ package feign.codec;
import feign.FeignException;
import feign.Response;
import feign.Util;
import java.io.IOException;
import java.lang.reflect.Type;
@ -76,5 +77,16 @@ public interface Decoder { @@ -76,5 +77,16 @@ public interface Decoder {
* Default implementation of {@code Decoder}.
*/
public class Default extends StringDecoder {
@Override
public Object decode(Response response, Type type) throws IOException {
Response.Body body = response.body();
if (body == null) {
return null;
}
if (byte[].class.equals(type)) {
return Util.toByteArray(body.asInputStream());
}
return super.decode(response, type);
}
}
}

4
core/src/main/java/feign/codec/Encoder.java

@ -69,13 +69,15 @@ public interface Encoder { @@ -69,13 +69,15 @@ public interface Encoder {
void encode(Object object, RequestTemplate template) throws EncodeException;
/**
* Default implementation of {@code Encoder} that supports {@code String}s only.
* Default implementation of {@code Encoder}.
*/
public class Default implements Encoder {
@Override
public void encode(Object object, RequestTemplate template) throws EncodeException {
if (object instanceof String) {
template.body(object.toString());
} else if (object instanceof byte[]) {
template.body((byte[]) object, null);
} else if (object != null) {
throw new EncodeException(format("%s is not a type supported by this encoder.", object.getClass()));
}

5
core/src/test/java/feign/DefaultContractTest.java

@ -29,6 +29,8 @@ import static org.testng.Assert.assertFalse; @@ -29,6 +29,8 @@ import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
import static feign.Util.UTF_8;
/**
* Tests interfaces defined per {@link Contract.Default} are interpreted into expected {@link feign
* .RequestTemplate template}
@ -154,8 +156,9 @@ public class DefaultContractTest { @@ -154,8 +156,9 @@ public class DefaultContractTest {
}
@Test public void bodyWithoutParameters() throws Exception {
String expectedBody = "<v01:getAccountsListOfUser/>";
MethodMetadata md = contract.parseAndValidatateMetadata(BodyWithoutParameters.class.getDeclaredMethod("post"));
assertEquals(md.template().body(), "<v01:getAccountsListOfUser/>");
assertEquals(md.template().body(), expectedBody.getBytes(UTF_8));
assertFalse(md.template().bodyTemplate() != null);
assertTrue(md.formParams().isEmpty());
assertTrue(md.indexToName().isEmpty());

37
core/src/test/java/feign/FeignTest.java

@ -113,6 +113,10 @@ public class FeignTest { @@ -113,6 +113,10 @@ public class FeignTest {
interface OtherTestInterface {
@RequestLine("POST /") String post();
@RequestLine("POST /") byte[] binaryResponseBody();
@RequestLine("POST /") void binaryRequestBody(byte[] contents);
}
@Module(library = true, overrides = true)
@ -499,4 +503,37 @@ public class FeignTest { @@ -499,4 +503,37 @@ public class FeignTest {
assertEquals(i1.hashCode(), i1.hashCode());
assertEquals(i1.hashCode(), i2.hashCode());
}
@Test public void decodeLogicSupportsByteArray() throws Exception {
byte[] expectedResponse = {12, 34, 56};
final MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setBody(expectedResponse));
server.play();
try {
OtherTestInterface api = Feign.builder().target(OtherTestInterface.class, "http://localhost:" + server.getPort());
byte[] actualResponse = api.binaryResponseBody();
assertEquals(actualResponse, expectedResponse);
} finally {
server.shutdown();
}
}
@Test public void encodeLogicSupportsByteArray() throws Exception {
byte[] expectedRequest = {12, 34, 56};
final MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse());
server.play();
try {
OtherTestInterface api = Feign.builder().target(OtherTestInterface.class, "http://localhost:" + server.getPort());
api.binaryRequestBody(expectedRequest);
byte[] actualRequest = server.takeRequest().getBody();
assertEquals(actualRequest, expectedRequest);
} finally {
server.shutdown();
}
}
}

16
core/src/test/java/feign/codec/DefaultDecoderTest.java

@ -19,7 +19,8 @@ import feign.Response; @@ -19,7 +19,8 @@ import feign.Response;
import org.testng.annotations.Test;
import org.w3c.dom.Document;
import java.io.StringReader;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
@ -28,6 +29,8 @@ import java.util.Map; @@ -28,6 +29,8 @@ import java.util.Map;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNull;
import static feign.Util.UTF_8;
public class DefaultDecoderTest {
private final Decoder decoder = new Decoder.Default();
@ -38,6 +41,13 @@ public class DefaultDecoderTest { @@ -38,6 +41,13 @@ public class DefaultDecoderTest {
assertEquals(decodedObject.toString(), "response body");
}
@Test public void testDecodesToByteArray() throws Exception {
Response response = knownResponse();
Object decodedObject = decoder.decode(response, byte[].class);
assertEquals(decodedObject.getClass(), byte[].class);
assertEquals((byte[]) decodedObject, "response body".getBytes(UTF_8));
}
@Test public void testDecodesNullBodyToNull() throws Exception {
assertNull(decoder.decode(nullBodyResponse(), Document.class));
}
@ -49,10 +59,10 @@ public class DefaultDecoderTest { @@ -49,10 +59,10 @@ public class DefaultDecoderTest {
private Response knownResponse() {
String content = "response body";
StringReader reader = new StringReader(content);
InputStream inputStream = new ByteArrayInputStream(content.getBytes(UTF_8));
Map<String, Collection<String>> headers = new HashMap<String, Collection<String>>();
headers.put("Content-Type", Collections.singleton("text/plain"));
return Response.create(200, "OK", headers, reader, content.length());
return Response.create(200, "OK", headers, inputStream, content.length());
}
private Response nullBodyResponse() {

9
core/src/test/java/feign/codec/DefaultEncoderTest.java

@ -22,6 +22,8 @@ import java.util.Date; @@ -22,6 +22,8 @@ import java.util.Date;
import static org.testng.Assert.assertEquals;
import static feign.Util.UTF_8;
public class DefaultEncoderTest {
private final Encoder encoder = new Encoder.Default();
@ -29,6 +31,13 @@ public class DefaultEncoderTest { @@ -29,6 +31,13 @@ public class DefaultEncoderTest {
String content = "This is my content";
RequestTemplate template = new RequestTemplate();
encoder.encode(content, template);
assertEquals(template.body(), content.getBytes(UTF_8));
}
@Test public void testEncodesByteArray() throws Exception {
byte[] content = {12, 34, 56};
RequestTemplate template = new RequestTemplate();
encoder.encode(content, template);
assertEquals(template.body(), content);
}

3
core/src/test/java/feign/codec/DefaultErrorDecoderTest.java

@ -27,6 +27,7 @@ import feign.Response; @@ -27,6 +27,7 @@ import feign.Response;
import feign.RetryableException;
import static feign.Util.RETRY_AFTER;
import static feign.Util.UTF_8;
public class DefaultErrorDecoderTest {
ErrorDecoder errorDecoder = new ErrorDecoder.Default();
@ -42,7 +43,7 @@ public class DefaultErrorDecoderTest { @@ -42,7 +43,7 @@ public class DefaultErrorDecoderTest {
@Test(expectedExceptions = FeignException.class, expectedExceptionsMessageRegExp = "status 500 reading Service#foo\\(\\); content:\nhello world")
public void throwsFeignExceptionIncludingBody() throws Throwable {
Response response = Response.create(500, "Internal server error", ImmutableMap.<String, Collection<String>>of(),
"hello world");
"hello world", UTF_8);
throw errorDecoder.decode("Service#foo()", response);
}

35
gson/src/test/java/feign/gson/GsonModuleTest.java

@ -40,6 +40,8 @@ import java.util.Map; @@ -40,6 +40,8 @@ import java.util.Map;
import static org.testng.Assert.assertEquals;
import static feign.Util.UTF_8;
@Test
public class GsonModuleTest {
@Module(includes = GsonModule.class, injects = EncoderAndDecoderBindings.class)
@ -62,6 +64,11 @@ public class GsonModuleTest { @@ -62,6 +64,11 @@ public class GsonModuleTest {
}
@Test public void encodesMapObjectNumericalValuesAsInteger() throws Exception {
String expectedBody = ""
+ "{\n"
+ " \"foo\": 1\n"
+ "}";
EncoderBindings bindings = new EncoderBindings();
ObjectGraph.create(bindings).inject(bindings);
@ -70,13 +77,18 @@ public class GsonModuleTest { @@ -70,13 +77,18 @@ public class GsonModuleTest {
RequestTemplate template = new RequestTemplate();
bindings.encoder.encode(map, template);
assertEquals(template.body(), ""//
+ "{\n" //
+ " \"foo\": 1\n" //
+ "}");
assertEquals(template.body(), expectedBody.getBytes(UTF_8));
}
@Test public void encodesFormParams() throws Exception {
String expectedBody = ""//
+ "{\n" //
+ " \"foo\": 1,\n" //
+ " \"bar\": [\n" //
+ " 2,\n" //
+ " 3\n" //
+ " ]\n" //
+ "}";
EncoderBindings bindings = new EncoderBindings();
ObjectGraph.create(bindings).inject(bindings);
@ -87,14 +99,7 @@ public class GsonModuleTest { @@ -87,14 +99,7 @@ public class GsonModuleTest {
RequestTemplate template = new RequestTemplate();
bindings.encoder.encode(form, template);
assertEquals(template.body(), ""//
+ "{\n" //
+ " \"foo\": 1,\n" //
+ " \"bar\": [\n" //
+ " 2,\n" //
+ " 3\n" //
+ " ]\n" //
+ "}");
assertEquals(template.body(), expectedBody.getBytes(UTF_8));
}
static class Zone extends LinkedHashMap<String, Object> {
@ -128,7 +133,8 @@ public class GsonModuleTest { @@ -128,7 +133,8 @@ public class GsonModuleTest {
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);
Response response =
Response.create(200, "OK", Collections.<String, Collection<String>>emptyMap(), zonesJson, UTF_8);
assertEquals(bindings.decoder.decode(response, new TypeToken<List<Zone>>() {
}.getType()), zones);
}
@ -184,7 +190,8 @@ public class GsonModuleTest { @@ -184,7 +190,8 @@ public class GsonModuleTest {
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);
Response response =
Response.create(200, "OK", Collections.<String, Collection<String>>emptyMap(), zonesJson, UTF_8);
assertEquals(bindings.decoder.decode(response, new TypeToken<List<Zone>>() {
}.getType()), zones);
}

6
jackson/src/main/java/feign/jackson/JacksonDecoder.java

@ -22,7 +22,7 @@ import feign.Response; @@ -22,7 +22,7 @@ import feign.Response;
import feign.codec.Decoder;
import java.io.IOException;
import java.io.Reader;
import java.io.InputStream;
import java.lang.reflect.Type;
public class JacksonDecoder implements Decoder {
@ -40,9 +40,9 @@ public class JacksonDecoder implements Decoder { @@ -40,9 +40,9 @@ public class JacksonDecoder implements Decoder {
if (response.body() == null) {
return null;
}
Reader reader = response.body().asReader();
InputStream inputStream = response.body().asInputStream();
try {
return mapper.readValue(reader, mapper.constructType(type));
return mapper.readValue(inputStream, mapper.constructType(type));
} catch (RuntimeJsonMappingException e) {
if (e.getCause() != null && e.getCause() instanceof IOException) {
throw IOException.class.cast(e.getCause());

12
jackson/src/test/java/feign/jackson/JacksonModuleTest.java

@ -21,6 +21,8 @@ import java.util.*; @@ -21,6 +21,8 @@ import java.util.*;
import static org.testng.Assert.assertEquals;
import static feign.Util.UTF_8;
@Test
public class JacksonModuleTest {
@Module(includes = JacksonModule.class, injects = EncoderAndDecoderBindings.class)
@ -54,7 +56,7 @@ public class JacksonModuleTest { @@ -54,7 +56,7 @@ public class JacksonModuleTest {
RequestTemplate template = new RequestTemplate();
bindings.encoder.encode(map, template);
assertEquals(template.body(), ""//
assertEquals(new String(template.body(), UTF_8), ""//
+ "{\n" //
+ " \"foo\" : 1\n" //
+ "}");
@ -70,7 +72,7 @@ public class JacksonModuleTest { @@ -70,7 +72,7 @@ public class JacksonModuleTest {
RequestTemplate template = new RequestTemplate();
bindings.encoder.encode(form, template);
assertEquals(template.body(), ""//
assertEquals(new String(template.body(), UTF_8), ""//
+ "{\n" //
+ " \"foo\" : 1,\n" //
+ " \"bar\" : [ 2, 3 ]\n" //
@ -109,7 +111,8 @@ public class JacksonModuleTest { @@ -109,7 +111,8 @@ public class JacksonModuleTest {
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);
Response response =
Response.create(200, "OK", Collections.<String, Collection<String>>emptyMap(), zonesJson, UTF_8);
assertEquals(bindings.decoder.decode(response, new TypeToken<List<Zone>>() {
}.getType()), zones);
}
@ -177,7 +180,8 @@ public class JacksonModuleTest { @@ -177,7 +180,8 @@ public class JacksonModuleTest {
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);
Response response =
Response.create(200, "OK", Collections.<String, Collection<String>>emptyMap(), zonesJson, UTF_8);
assertEquals(bindings.decoder.decode(response, new TypeToken<List<Zone>>() {
}.getType()), zones);
}

3
ribbon/src/main/java/feign/ribbon/LBClient.java

@ -93,7 +93,8 @@ class LBClient extends AbstractLoadBalancerAwareClient<LBClient.RibbonRequest, L @@ -93,7 +93,8 @@ class LBClient extends AbstractLoadBalancerAwareClient<LBClient.RibbonRequest, L
.method(request.method())
.append(getUri().toASCIIString())
.headers(request.headers())
.body(request.body()).request();
.body(request.body(), request.charset())
.request();
}
public Object clone() {

8
sax/src/main/java/feign/sax/SAXDecoder.java

@ -26,7 +26,7 @@ import org.xml.sax.helpers.XMLReaderFactory; @@ -26,7 +26,7 @@ import org.xml.sax.helpers.XMLReaderFactory;
import javax.inject.Provider;
import java.io.IOException;
import java.io.Reader;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Type;
import java.util.LinkedHashMap;
@ -154,11 +154,11 @@ public class SAXDecoder implements Decoder { @@ -154,11 +154,11 @@ public class SAXDecoder implements Decoder {
xmlReader.setFeature("http://xml.org/sax/features/namespaces", false);
xmlReader.setFeature("http://xml.org/sax/features/validation", false);
xmlReader.setContentHandler(handler);
Reader reader = response.body().asReader();
InputStream inputStream = response.body().asInputStream();
try {
xmlReader.parse(new InputSource(reader));
xmlReader.parse(new InputSource(inputStream));
} finally {
ensureClosed(reader);
ensureClosed(inputStream);
}
return handler.result();
} catch (SAXException e) {

4
sax/src/test/java/feign/sax/SAXDecoderTest.java

@ -32,6 +32,8 @@ import java.util.Collections; @@ -32,6 +32,8 @@ import java.util.Collections;
import static org.testng.Assert.assertEquals;
import static feign.Util.UTF_8;
// unbound wildcards are not currently injectable in dagger.
@SuppressWarnings("rawtypes")
public class SAXDecoderTest {
@ -64,7 +66,7 @@ public class SAXDecoderTest { @@ -64,7 +66,7 @@ public class SAXDecoderTest {
}
private Response statusFailedResponse() {
return Response.create(200, "OK", Collections.<String, Collection<String>>emptyMap(), statusFailed);
return Response.create(200, "OK", Collections.<String, Collection<String>>emptyMap(), statusFailed, UTF_8);
}
static String statusFailed = ""//

7
sax/src/test/java/feign/sax/examples/AWSSignatureVersion4.java

@ -128,9 +128,10 @@ public class AWSSignatureVersion4 implements Function<RequestTemplate, Request> @@ -128,9 +128,10 @@ public class AWSSignatureVersion4 implements Function<RequestTemplate, Request>
canonicalRequest.append(Joiner.on(',').join(sortedLowercaseHeaders.keySet())).append('\n');
// HexEncode(Hash(Payload))
if (input.body() != null) {
canonicalRequest.append(base16().lowerCase().encode(
sha256().hashString(input.body() != null ? input.body() : "", UTF_8).asBytes()));
String bodyText =
input.charset() != null && input.body() != null ? new String(input.body(), input.charset()) : null;
if (bodyText != null) {
canonicalRequest.append(base16().lowerCase().encode(sha256().hashString(bodyText, UTF_8).asBytes()));
} else {
canonicalRequest.append(EMPTY_STRING_HASH);
}

Loading…
Cancel
Save