From 6d431e3a2f8002f2bd2e3852691cd1ddd579d5fe Mon Sep 17 00:00:00 2001 From: shiya Date: Fri, 10 Aug 2018 13:42:00 +0800 Subject: [PATCH 1/7] add charset to Response.Body when create Reader --- core/src/main/java/feign/Response.java | 554 +++++++++--------- .../feign/httpclient/ApacheHttpClient.java | 321 +++++----- .../main/java/feign/okhttp/OkHttpClient.java | 6 + 3 files changed, 461 insertions(+), 420 deletions(-) diff --git a/core/src/main/java/feign/Response.java b/core/src/main/java/feign/Response.java index 366f275188..65a41d98ba 100644 --- a/core/src/main/java/feign/Response.java +++ b/core/src/main/java/feign/Response.java @@ -1,11 +1,11 @@ /** * Copyright 2012-2018 The Feign Authors - * + *

* 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 @@ -26,309 +26,335 @@ import java.util.Locale; import java.util.Map; import java.util.TreeMap; -import static feign.Util.UTF_8; -import static feign.Util.checkNotNull; -import static feign.Util.checkState; -import static feign.Util.decodeOrDefault; -import static feign.Util.valuesOrEmpty; + +import static feign.Util.*; /** * An immutable response to an http invocation which only returns string content. */ public final class Response implements Closeable { - private final int status; - private final String reason; - private final Map> headers; - private final Body body; - private final Request request; - - private Response(Builder builder) { - checkState(builder.status >= 200, "Invalid status code: %s", builder.status); - checkState(builder.request != null, "original request is required"); - this.status = builder.status; - this.request = builder.request; - this.reason = builder.reason; // nullable - this.headers = Collections.unmodifiableMap(caseInsensitiveCopyOf(builder.headers)); - this.body = builder.body; // nullable - } - - public Builder toBuilder() { - return new Builder(this); - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - int status; - String reason; - Map> headers; - Body body; - Request request; - - Builder() {} - - Builder(Response source) { - this.status = source.status; - this.reason = source.reason; - this.headers = source.headers; - this.body = source.body; - this.request = source.request; - } - - /** @see Response#status */ - public Builder status(int status) { - this.status = status; - return this; - } - - /** @see Response#reason */ - public Builder reason(String reason) { - this.reason = reason; - return this; - } - - /** @see Response#headers */ - public Builder headers(Map> headers) { - this.headers = headers; - return this; + private final int status; + private final String reason; + private final Map> headers; + private final Body body; + private final Request request; + + private Response(Builder builder) { + checkState(builder.status >= 200, "Invalid status code: %s", builder.status); + checkState(builder.request != null, "original request is required"); + this.status = builder.status; + this.request = builder.request; + this.reason = builder.reason; // nullable + this.headers = Collections.unmodifiableMap(caseInsensitiveCopyOf(builder.headers)); + this.body = builder.body; // nullable } - /** @see Response#body */ - public Builder body(Body body) { - this.body = body; - return this; + public Builder toBuilder() { + return new Builder(this); } - /** @see Response#body */ - public Builder body(InputStream inputStream, Integer length) { - this.body = InputStreamBody.orNull(inputStream, length); - return this; + public static Builder builder() { + return new Builder(); } - /** @see Response#body */ - public Builder body(byte[] data) { - this.body = ByteArrayBody.orNull(data); - return this; - } - - /** @see Response#body */ - public Builder body(String text, Charset charset) { - this.body = ByteArrayBody.orNull(text, charset); - return this; + public static final class Builder { + int status; + String reason; + Map> headers; + Body body; + Request request; + + Builder() { + } + + Builder(Response source) { + this.status = source.status; + this.reason = source.reason; + this.headers = source.headers; + this.body = source.body; + this.request = source.request; + } + + /** + * @see Response#status + */ + public Builder status(int status) { + this.status = status; + return this; + } + + /** + * @see Response#reason + */ + public Builder reason(String reason) { + this.reason = reason; + return this; + } + + /** + * @see Response#headers + */ + public Builder headers(Map> headers) { + this.headers = headers; + return this; + } + + /** + * @see Response#body + */ + public Builder body(Body body) { + this.body = body; + return this; + } + + /** + * @see Response#body + */ + public Builder body(InputStream inputStream, Integer length) { + this.body = InputStreamBody.orNull(inputStream, length); + return this; + } + + /** + * @see Response#body + */ + public Builder body(byte[] data) { + this.body = ByteArrayBody.orNull(data); + return this; + } + + /** + * @see Response#body + */ + public Builder body(String text, Charset charset) { + this.body = ByteArrayBody.orNull(text, charset); + return this; + } + + /** + * @see Response#request + */ + public Builder request(Request request) { + checkNotNull(request, "request is required"); + this.request = request; + return this; + } + + public Response build() { + return new Response(this); + } } /** - * @see Response#request + * status code. ex {@code 200} + *

+ * See rfc2616 */ - public Builder request(Request request) { - checkNotNull(request, "request is required"); - this.request = request; - return this; - } - - public Response build() { - return new Response(this); - } - } - - /** - * status code. ex {@code 200} - * - * See rfc2616 - */ - public int status() { - return status; - } - - /** - * Nullable and not set when using http/2 - * - * See https://github.com/http2/http2-spec/issues/202 - */ - public String reason() { - return reason; - } - - /** - * Returns a case-insensitive mapping of header names to their values. - */ - public Map> headers() { - return headers; - } - - /** - * if present, the response had a body - */ - public Body body() { - return body; - } - - /** - * the request that generated this response - */ - public Request request() { - return request; - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder("HTTP/1.1 ").append(status); - if (reason != null) - builder.append(' ').append(reason); - builder.append('\n'); - for (String field : headers.keySet()) { - for (String value : valuesOrEmpty(headers, field)) { - builder.append(field).append(": ").append(value).append('\n'); - } + public int status() { + return status; } - if (body != null) - builder.append('\n').append(body); - return builder.toString(); - } - - @Override - public void close() { - Util.ensureClosed(body); - } - - public interface Body extends Closeable { /** - * length in bytes, if known. Null if unknown or greater than {@link Integer#MAX_VALUE}. - * - *
- *
- *
- * Note
- * This is an integer as most implementations cannot do bodies greater than 2GB. + * Nullable and not set when using http/2 + *

+ * See https://github.com/http2/http2-spec/issues/202 */ - Integer length(); + public String reason() { + return reason; + } /** - * True if {@link #asInputStream()} and {@link #asReader()} can be called more than once. + * Returns a case-insensitive mapping of header names to their values. */ - boolean isRepeatable(); + public Map> headers() { + return headers; + } /** - * It is the responsibility of the caller to close the stream. + * if present, the response had a body */ - InputStream asInputStream() throws IOException; + public Body body() { + return body; + } /** - * It is the responsibility of the caller to close the stream. + * the request that generated this response */ - Reader asReader() throws IOException; - } - - private static final class InputStreamBody implements Response.Body { - - private final InputStream inputStream; - private final Integer length; - - private InputStreamBody(InputStream inputStream, Integer length) { - this.inputStream = inputStream; - this.length = length; - } - - private static Body orNull(InputStream inputStream, Integer length) { - if (inputStream == null) { - return null; - } - return new InputStreamBody(inputStream, length); - } - - @Override - public Integer length() { - return length; + public Request request() { + return request; } @Override - public boolean isRepeatable() { - return false; - } - - @Override - public InputStream asInputStream() throws IOException { - return inputStream; - } - - @Override - public Reader asReader() throws IOException { - return new InputStreamReader(inputStream, UTF_8); - } - - @Override - public void close() throws IOException { - inputStream.close(); - } - } - - private static final class ByteArrayBody implements Response.Body { - - private final byte[] data; - - public ByteArrayBody(byte[] data) { - this.data = data; - } - - private static Body orNull(byte[] data) { - if (data == null) { - return null; - } - return new ByteArrayBody(data); - } - - private static Body orNull(String text, Charset charset) { - if (text == null) { - return null; - } - checkNotNull(charset, "charset"); - return new ByteArrayBody(text.getBytes(charset)); + public String toString() { + StringBuilder builder = new StringBuilder("HTTP/1.1 ").append(status); + if (reason != null) + builder.append(' ').append(reason); + builder.append('\n'); + for (String field : headers.keySet()) { + for (String value : valuesOrEmpty(headers, field)) { + builder.append(field).append(": ").append(value).append('\n'); + } + } + if (body != null) + builder.append('\n').append(body); + return builder.toString(); } @Override - public Integer length() { - return data.length; + public void close() { + Util.ensureClosed(body); } - @Override - public boolean isRepeatable() { - return true; + public interface Body extends Closeable { + + /** + * length in bytes, if known. Null if unknown or greater than {@link Integer#MAX_VALUE}. + *

+ *
+ *
+ *
+ * Note
+ * This is an integer as most implementations cannot do bodies greater than 2GB. + */ + Integer length(); + + /** + * 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; + + /** + * for custom charset + */ + Reader asReader(Charset charset) throws IOException; } - @Override - public InputStream asInputStream() throws IOException { - return new ByteArrayInputStream(data); + private static final class InputStreamBody implements Response.Body { + + private final InputStream inputStream; + private final Integer length; + + private InputStreamBody(InputStream inputStream, Integer length) { + this.inputStream = inputStream; + this.length = length; + } + + private static Body orNull(InputStream inputStream, Integer length) { + if (inputStream == null) { + return null; + } + return new InputStreamBody(inputStream, length); + } + + @Override + public Integer length() { + return length; + } + + @Override + public boolean isRepeatable() { + return false; + } + + @Override + public InputStream asInputStream() throws IOException { + return inputStream; + } + + @Override + public Reader asReader() throws IOException { + return asReader(UTF_8); + } + + public Reader asReader(Charset charset) throws IOException { + return new InputStreamReader(asInputStream(), charset == null ? UTF_8 : charset); + } + + @Override + public void close() throws IOException { + inputStream.close(); + } } - @Override - public Reader asReader() throws IOException { - return new InputStreamReader(asInputStream(), UTF_8); + private static final class ByteArrayBody implements Response.Body { + + private final byte[] data; + + public ByteArrayBody(byte[] data) { + this.data = data; + } + + private static Body orNull(byte[] data) { + if (data == null) { + return null; + } + return new ByteArrayBody(data); + } + + private static Body orNull(String text, Charset charset) { + if (text == null) { + return null; + } + checkNotNull(charset, "charset"); + return new ByteArrayBody(text.getBytes(charset)); + } + + @Override + public Integer 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 InputStreamReader(asInputStream(), UTF_8); + } + + public Reader asReader(Charset charset) throws IOException { + return new InputStreamReader(asInputStream(), charset == null ? UTF_8 : charset); + } + + @Override + public void close() throws IOException { + } + + @Override + public String toString() { + return decodeOrDefault(data, UTF_8, "Binary data"); + } } - @Override - public void close() throws IOException {} - - @Override - public String toString() { - return decodeOrDefault(data, UTF_8, "Binary data"); - } - } - - private static Map> caseInsensitiveCopyOf(Map> headers) { - Map> result = - new TreeMap>(String.CASE_INSENSITIVE_ORDER); - - for (Map.Entry> entry : headers.entrySet()) { - String headerName = entry.getKey(); - if (!result.containsKey(headerName)) { - result.put(headerName.toLowerCase(Locale.ROOT), new LinkedList()); - } - result.get(headerName).addAll(entry.getValue()); + private static Map> caseInsensitiveCopyOf(Map> headers) { + Map> result = + new TreeMap>(String.CASE_INSENSITIVE_ORDER); + + for (Map.Entry> entry : headers.entrySet()) { + String headerName = entry.getKey(); + if (!result.containsKey(headerName)) { + result.put(headerName.toLowerCase(Locale.ROOT), new LinkedList()); + } + result.get(headerName).addAll(entry.getValue()); + } + return result; } - return result; - } } diff --git a/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java b/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java index 0a16ac6192..5139af46a7 100644 --- a/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java +++ b/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java @@ -1,11 +1,11 @@ /** * Copyright 2012-2018 The Feign Authors - * + *

* 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 @@ -30,6 +30,7 @@ import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.util.EntityUtils; + import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -38,21 +39,24 @@ import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; + import feign.Client; import feign.Request; import feign.Response; import feign.Util; + import static feign.Util.UTF_8; /** * This module directs Feign's http requests to Apache's * HttpClient. Ex. - * + * *

  * GitHub github = Feign.builder().client(new ApacheHttpClient()).target(GitHub.class,
  * "https://api.github.com");
@@ -61,173 +65,178 @@
  * Based on Square, Inc's Retrofit ApacheClient implementation
  */
 public final class ApacheHttpClient implements Client {
-  private static final String ACCEPT_HEADER_NAME = "Accept";
+    private static final String ACCEPT_HEADER_NAME = "Accept";
 
-  private final HttpClient client;
+    private final HttpClient client;
 
-  public ApacheHttpClient() {
-    this(HttpClientBuilder.create().build());
-  }
+    public ApacheHttpClient() {
+        this(HttpClientBuilder.create().build());
+    }
 
-  public ApacheHttpClient(HttpClient client) {
-    this.client = client;
-  }
+    public ApacheHttpClient(HttpClient client) {
+        this.client = client;
+    }
 
-  @Override
-  public Response execute(Request request, Request.Options options) throws IOException {
-    HttpUriRequest httpUriRequest;
-    try {
-      httpUriRequest = toHttpUriRequest(request, options);
-    } catch (URISyntaxException e) {
-      throw new IOException("URL '" + request.url() + "' couldn't be parsed into a URI", e);
+    @Override
+    public Response execute(Request request, Request.Options options) throws IOException {
+        HttpUriRequest httpUriRequest;
+        try {
+            httpUriRequest = toHttpUriRequest(request, options);
+        } catch (URISyntaxException e) {
+            throw new IOException("URL '" + request.url() + "' couldn't be parsed into a URI", e);
+        }
+        HttpResponse httpResponse = client.execute(httpUriRequest);
+        return toFeignResponse(httpResponse, request);
     }
-    HttpResponse httpResponse = client.execute(httpUriRequest);
-    return toFeignResponse(httpResponse, request);
-  }
-
-  HttpUriRequest toHttpUriRequest(Request request, Request.Options options)
-      throws UnsupportedEncodingException, MalformedURLException, URISyntaxException {
-    RequestBuilder requestBuilder = RequestBuilder.create(request.method());
-
-    // per request timeouts
-    RequestConfig requestConfig =
-        (client instanceof Configurable ? RequestConfig.copy(((Configurable) client).getConfig())
-            : RequestConfig.custom())
-                .setConnectTimeout(options.connectTimeoutMillis())
-                .setSocketTimeout(options.readTimeoutMillis())
-                .build();
-    requestBuilder.setConfig(requestConfig);
 
-    URI uri = new URIBuilder(request.url()).build();
+    HttpUriRequest toHttpUriRequest(Request request, Request.Options options)
+            throws UnsupportedEncodingException, MalformedURLException, URISyntaxException {
+        RequestBuilder requestBuilder = RequestBuilder.create(request.method());
 
-    requestBuilder.setUri(uri.getScheme() + "://" + uri.getAuthority() + uri.getRawPath());
+        // per request timeouts
+        RequestConfig requestConfig =
+                (client instanceof Configurable ? RequestConfig.copy(((Configurable) client).getConfig())
+                        : RequestConfig.custom())
+                        .setConnectTimeout(options.connectTimeoutMillis())
+                        .setSocketTimeout(options.readTimeoutMillis())
+                        .build();
+        requestBuilder.setConfig(requestConfig);
 
-    // request query params
-    List queryParams =
-        URLEncodedUtils.parse(uri, requestBuilder.getCharset().name());
-    for (NameValuePair queryParam : queryParams) {
-      requestBuilder.addParameter(queryParam);
-    }
+        URI uri = new URIBuilder(request.url()).build();
 
-    // request headers
-    boolean hasAcceptHeader = false;
-    for (Map.Entry> headerEntry : request.headers().entrySet()) {
-      String headerName = headerEntry.getKey();
-      if (headerName.equalsIgnoreCase(ACCEPT_HEADER_NAME)) {
-        hasAcceptHeader = true;
-      }
-
-      if (headerName.equalsIgnoreCase(Util.CONTENT_LENGTH)) {
-        // The 'Content-Length' header is always set by the Apache client and it
-        // doesn't like us to set it as well.
-        continue;
-      }
-
-      for (String headerValue : headerEntry.getValue()) {
-        requestBuilder.addHeader(headerName, headerValue);
-      }
-    }
-    // some servers choke on the default accept string, so we'll set it to anything
-    if (!hasAcceptHeader) {
-      requestBuilder.addHeader(ACCEPT_HEADER_NAME, "*/*");
+        requestBuilder.setUri(uri.getScheme() + "://" + uri.getAuthority() + uri.getRawPath());
+
+        // request query params
+        List queryParams =
+                URLEncodedUtils.parse(uri, requestBuilder.getCharset().name());
+        for (NameValuePair queryParam : queryParams) {
+            requestBuilder.addParameter(queryParam);
+        }
+
+        // request headers
+        boolean hasAcceptHeader = false;
+        for (Map.Entry> headerEntry : request.headers().entrySet()) {
+            String headerName = headerEntry.getKey();
+            if (headerName.equalsIgnoreCase(ACCEPT_HEADER_NAME)) {
+                hasAcceptHeader = true;
+            }
+
+            if (headerName.equalsIgnoreCase(Util.CONTENT_LENGTH)) {
+                // The 'Content-Length' header is always set by the Apache client and it
+                // doesn't like us to set it as well.
+                continue;
+            }
+
+            for (String headerValue : headerEntry.getValue()) {
+                requestBuilder.addHeader(headerName, headerValue);
+            }
+        }
+        // some servers choke on the default accept string, so we'll set it to anything
+        if (!hasAcceptHeader) {
+            requestBuilder.addHeader(ACCEPT_HEADER_NAME, "*/*");
+        }
+
+        // request body
+        if (request.body() != null) {
+            HttpEntity entity = null;
+            if (request.charset() != null) {
+                ContentType contentType = getContentType(request);
+                String content = new String(request.body(), request.charset());
+                entity = new StringEntity(content, contentType);
+            } else {
+                entity = new ByteArrayEntity(request.body());
+            }
+
+            requestBuilder.setEntity(entity);
+        } else {
+            requestBuilder.setEntity(new ByteArrayEntity(new byte[0]));
+        }
+
+        return requestBuilder.build();
     }
 
-    // request body
-    if (request.body() != null) {
-      HttpEntity entity = null;
-      if (request.charset() != null) {
-        ContentType contentType = getContentType(request);
-        String content = new String(request.body(), request.charset());
-        entity = new StringEntity(content, contentType);
-      } else {
-        entity = new ByteArrayEntity(request.body());
-      }
-
-      requestBuilder.setEntity(entity);
-    } else {
-      requestBuilder.setEntity(new ByteArrayEntity(new byte[0]));
+    private ContentType getContentType(Request request) {
+        ContentType contentType = ContentType.DEFAULT_TEXT;
+        for (Map.Entry> entry : request.headers().entrySet())
+            if (entry.getKey().equalsIgnoreCase("Content-Type")) {
+                Collection values = entry.getValue();
+                if (values != null && !values.isEmpty()) {
+                    contentType = ContentType.parse(values.iterator().next());
+                    if (contentType.getCharset() == null) {
+                        contentType = contentType.withCharset(request.charset());
+                    }
+                    break;
+                }
+            }
+        return contentType;
     }
 
-    return requestBuilder.build();
-  }
-
-  private ContentType getContentType(Request request) {
-    ContentType contentType = ContentType.DEFAULT_TEXT;
-    for (Map.Entry> entry : request.headers().entrySet())
-      if (entry.getKey().equalsIgnoreCase("Content-Type")) {
-        Collection values = entry.getValue();
-        if (values != null && !values.isEmpty()) {
-          contentType = ContentType.parse(values.iterator().next());
-          if (contentType.getCharset() == null) {
-            contentType = contentType.withCharset(request.charset());
-          }
-          break;
+    Response toFeignResponse(HttpResponse httpResponse, Request request) throws IOException {
+        StatusLine statusLine = httpResponse.getStatusLine();
+        int statusCode = statusLine.getStatusCode();
+
+        String reason = statusLine.getReasonPhrase();
+
+        Map> headers = new HashMap>();
+        for (Header header : httpResponse.getAllHeaders()) {
+            String name = header.getName();
+            String value = header.getValue();
+
+            Collection headerValues = headers.get(name);
+            if (headerValues == null) {
+                headerValues = new ArrayList();
+                headers.put(name, headerValues);
+            }
+            headerValues.add(value);
         }
-      }
-    return contentType;
-  }
-
-  Response toFeignResponse(HttpResponse httpResponse, Request request) throws IOException {
-    StatusLine statusLine = httpResponse.getStatusLine();
-    int statusCode = statusLine.getStatusCode();
-
-    String reason = statusLine.getReasonPhrase();
-
-    Map> headers = new HashMap>();
-    for (Header header : httpResponse.getAllHeaders()) {
-      String name = header.getName();
-      String value = header.getValue();
-
-      Collection headerValues = headers.get(name);
-      if (headerValues == null) {
-        headerValues = new ArrayList();
-        headers.put(name, headerValues);
-      }
-      headerValues.add(value);
+
+        return Response.builder()
+                .status(statusCode)
+                .reason(reason)
+                .headers(headers)
+                .request(request)
+                .body(toFeignBody(httpResponse))
+                .build();
     }
 
-    return Response.builder()
-        .status(statusCode)
-        .reason(reason)
-        .headers(headers)
-        .request(request)
-        .body(toFeignBody(httpResponse))
-        .build();
-  }
-
-  Response.Body toFeignBody(HttpResponse httpResponse) throws IOException {
-    final HttpEntity entity = httpResponse.getEntity();
-    if (entity == null) {
-      return null;
+    Response.Body toFeignBody(HttpResponse httpResponse) throws IOException {
+        final HttpEntity entity = httpResponse.getEntity();
+        if (entity == null) {
+            return null;
+        }
+        return new Response.Body() {
+
+            @Override
+            public Integer length() {
+                return entity.getContentLength() >= 0 && entity.getContentLength() <= Integer.MAX_VALUE
+                        ? (int) entity.getContentLength()
+                        : null;
+            }
+
+            @Override
+            public boolean isRepeatable() {
+                return entity.isRepeatable();
+            }
+
+            @Override
+            public InputStream asInputStream() throws IOException {
+                return entity.getContent();
+            }
+
+            @Override
+            public Reader asReader() throws IOException {
+                return new InputStreamReader(asInputStream(), UTF_8);
+            }
+
+            @Override
+            public Reader asReader(Charset charset) throws IOException {
+                return new InputStreamReader(asInputStream(), charset == null ? UTF_8 : charset);
+            }
+
+            @Override
+            public void close() throws IOException {
+                EntityUtils.consume(entity);
+            }
+        };
     }
-    return new Response.Body() {
-
-      @Override
-      public Integer length() {
-        return entity.getContentLength() >= 0 && entity.getContentLength() <= Integer.MAX_VALUE
-            ? (int) entity.getContentLength()
-            : null;
-      }
-
-      @Override
-      public boolean isRepeatable() {
-        return entity.isRepeatable();
-      }
-
-      @Override
-      public InputStream asInputStream() throws IOException {
-        return entity.getContent();
-      }
-
-      @Override
-      public Reader asReader() throws IOException {
-        return new InputStreamReader(asInputStream(), UTF_8);
-      }
-
-      @Override
-      public void close() throws IOException {
-        EntityUtils.consume(entity);
-      }
-    };
-  }
 }
diff --git a/okhttp/src/main/java/feign/okhttp/OkHttpClient.java b/okhttp/src/main/java/feign/okhttp/OkHttpClient.java
index 94064c709b..bbe4670e1c 100644
--- a/okhttp/src/main/java/feign/okhttp/OkHttpClient.java
+++ b/okhttp/src/main/java/feign/okhttp/OkHttpClient.java
@@ -22,6 +22,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.Reader;
+import java.nio.charset.Charset;
 import java.util.Collection;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
@@ -142,6 +143,11 @@ public InputStream asInputStream() throws IOException {
       public Reader asReader() throws IOException {
         return input.charStream();
       }
+
+      @Override
+      public Reader asReader(Charset charset) throws IOException {
+        return asReader();
+      }
     };
   }
 

From 3f58718c7e402d1d4cf29b49f60f8fe5ef25d94a Mon Sep 17 00:00:00 2001
From: shiya 
Date: Fri, 10 Aug 2018 14:08:09 +0800
Subject: [PATCH 2/7] format

---
 core/src/main/java/feign/Response.java        | 569 +++++++++---------
 .../src/test/java/feign/FeignBuilderTest.java |  31 +-
 .../feign/httpclient/ApacheHttpClient.java    | 347 ++++++-----
 .../main/java/feign/okhttp/OkHttpClient.java  |  12 +-
 4 files changed, 483 insertions(+), 476 deletions(-)

diff --git a/core/src/main/java/feign/Response.java b/core/src/main/java/feign/Response.java
index 65a41d98ba..afb76f177a 100644
--- a/core/src/main/java/feign/Response.java
+++ b/core/src/main/java/feign/Response.java
@@ -13,6 +13,11 @@
  */
 package feign;
 
+import static feign.Util.UTF_8;
+import static feign.Util.checkNotNull;
+import static feign.Util.checkState;
+import static feign.Util.decodeOrDefault;
+import static feign.Util.valuesOrEmpty;
 import java.io.ByteArrayInputStream;
 import java.io.Closeable;
 import java.io.IOException;
@@ -27,334 +32,330 @@
 import java.util.Map;
 import java.util.TreeMap;
 
-import static feign.Util.*;
-
 /**
  * An immutable response to an http invocation which only returns string content.
  */
 public final class Response implements Closeable {
 
-    private final int status;
-    private final String reason;
-    private final Map> headers;
-    private final Body body;
-    private final Request request;
-
-    private Response(Builder builder) {
-        checkState(builder.status >= 200, "Invalid status code: %s", builder.status);
-        checkState(builder.request != null, "original request is required");
-        this.status = builder.status;
-        this.request = builder.request;
-        this.reason = builder.reason; // nullable
-        this.headers = Collections.unmodifiableMap(caseInsensitiveCopyOf(builder.headers));
-        this.body = builder.body; // nullable
+  private final int status;
+  private final String reason;
+  private final Map> headers;
+  private final Body body;
+  private final Request request;
+
+  private Response(Builder builder) {
+    checkState(builder.status >= 200, "Invalid status code: %s", builder.status);
+    checkState(builder.request != null, "original request is required");
+    this.status = builder.status;
+    this.request = builder.request;
+    this.reason = builder.reason; // nullable
+    this.headers = Collections.unmodifiableMap(caseInsensitiveCopyOf(builder.headers));
+    this.body = builder.body; // nullable
+  }
+
+  public Builder toBuilder() {
+    return new Builder(this);
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static final class Builder {
+    int status;
+    String reason;
+    Map> headers;
+    Body body;
+    Request request;
+
+    Builder() {}
+
+    Builder(Response source) {
+      this.status = source.status;
+      this.reason = source.reason;
+      this.headers = source.headers;
+      this.body = source.body;
+      this.request = source.request;
     }
 
-    public Builder toBuilder() {
-        return new Builder(this);
+    /**
+     * @see Response#status
+     */
+    public Builder status(int status) {
+      this.status = status;
+      return this;
     }
 
-    public static Builder builder() {
-        return new Builder();
+    /**
+     * @see Response#reason
+     */
+    public Builder reason(String reason) {
+      this.reason = reason;
+      return this;
     }
 
-    public static final class Builder {
-        int status;
-        String reason;
-        Map> headers;
-        Body body;
-        Request request;
-
-        Builder() {
-        }
-
-        Builder(Response source) {
-            this.status = source.status;
-            this.reason = source.reason;
-            this.headers = source.headers;
-            this.body = source.body;
-            this.request = source.request;
-        }
-
-        /**
-         * @see Response#status
-         */
-        public Builder status(int status) {
-            this.status = status;
-            return this;
-        }
-
-        /**
-         * @see Response#reason
-         */
-        public Builder reason(String reason) {
-            this.reason = reason;
-            return this;
-        }
-
-        /**
-         * @see Response#headers
-         */
-        public Builder headers(Map> headers) {
-            this.headers = headers;
-            return this;
-        }
-
-        /**
-         * @see Response#body
-         */
-        public Builder body(Body body) {
-            this.body = body;
-            return this;
-        }
-
-        /**
-         * @see Response#body
-         */
-        public Builder body(InputStream inputStream, Integer length) {
-            this.body = InputStreamBody.orNull(inputStream, length);
-            return this;
-        }
-
-        /**
-         * @see Response#body
-         */
-        public Builder body(byte[] data) {
-            this.body = ByteArrayBody.orNull(data);
-            return this;
-        }
-
-        /**
-         * @see Response#body
-         */
-        public Builder body(String text, Charset charset) {
-            this.body = ByteArrayBody.orNull(text, charset);
-            return this;
-        }
-
-        /**
-         * @see Response#request
-         */
-        public Builder request(Request request) {
-            checkNotNull(request, "request is required");
-            this.request = request;
-            return this;
-        }
-
-        public Response build() {
-            return new Response(this);
-        }
+    /**
+     * @see Response#headers
+     */
+    public Builder headers(Map> headers) {
+      this.headers = headers;
+      return this;
     }
 
     /**
-     * status code. ex {@code 200}
-     * 

- * See rfc2616 + * @see Response#body */ - public int status() { - return status; + public Builder body(Body body) { + this.body = body; + return this; } /** - * Nullable and not set when using http/2 - *

- * See https://github.com/http2/http2-spec/issues/202 + * @see Response#body */ - public String reason() { - return reason; + public Builder body(InputStream inputStream, Integer length) { + this.body = InputStreamBody.orNull(inputStream, length); + return this; } /** - * Returns a case-insensitive mapping of header names to their values. + * @see Response#body */ - public Map> headers() { - return headers; + public Builder body(byte[] data) { + this.body = ByteArrayBody.orNull(data); + return this; } /** - * if present, the response had a body + * @see Response#body */ - public Body body() { - return body; + public Builder body(String text, Charset charset) { + this.body = ByteArrayBody.orNull(text, charset); + return this; } /** - * the request that generated this response + * @see Response#request */ - public Request request() { - return request; + public Builder request(Request request) { + checkNotNull(request, "request is required"); + this.request = request; + return this; + } + + public Response build() { + return new Response(this); + } + } + + /** + * status code. ex {@code 200} + *

+ * See rfc2616 + */ + public int status() { + return status; + } + + /** + * Nullable and not set when using http/2 + *

+ * See https://github.com/http2/http2-spec/issues/202 + */ + public String reason() { + return reason; + } + + /** + * Returns a case-insensitive mapping of header names to their values. + */ + public Map> headers() { + return headers; + } + + /** + * if present, the response had a body + */ + public Body body() { + return body; + } + + /** + * the request that generated this response + */ + public Request request() { + return request; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("HTTP/1.1 ").append(status); + if (reason != null) + builder.append(' ').append(reason); + builder.append('\n'); + for (String field : headers.keySet()) { + for (String value : valuesOrEmpty(headers, field)) { + builder.append(field).append(": ").append(value).append('\n'); + } + } + if (body != null) + builder.append('\n').append(body); + return builder.toString(); + } + + @Override + public void close() { + Util.ensureClosed(body); + } + + public interface Body extends Closeable { + + /** + * length in bytes, if known. Null if unknown or greater than {@link Integer#MAX_VALUE}. + *

+ *
+ *
+ *
+ * Note
+ * This is an integer as most implementations cannot do bodies greater than 2GB. + */ + Integer length(); + + /** + * 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; + + /** + * for custom charset + */ + Reader asReader(Charset charset) throws IOException; + } + + private static final class InputStreamBody implements Response.Body { + + private final InputStream inputStream; + private final Integer length; + + private InputStreamBody(InputStream inputStream, Integer length) { + this.inputStream = inputStream; + this.length = length; + } + + private static Body orNull(InputStream inputStream, Integer length) { + if (inputStream == null) { + return null; + } + return new InputStreamBody(inputStream, length); } @Override - public String toString() { - StringBuilder builder = new StringBuilder("HTTP/1.1 ").append(status); - if (reason != null) - builder.append(' ').append(reason); - builder.append('\n'); - for (String field : headers.keySet()) { - for (String value : valuesOrEmpty(headers, field)) { - builder.append(field).append(": ").append(value).append('\n'); - } - } - if (body != null) - builder.append('\n').append(body); - return builder.toString(); + public Integer length() { + return length; + } + + @Override + public boolean isRepeatable() { + return false; + } + + @Override + public InputStream asInputStream() throws IOException { + return inputStream; + } + + @Override + public Reader asReader() throws IOException { + return asReader(UTF_8); + } + + public Reader asReader(Charset charset) throws IOException { + return new InputStreamReader(asInputStream(), charset == null ? UTF_8 : charset); + } + + @Override + public void close() throws IOException { + inputStream.close(); + } + } + + private static final class ByteArrayBody implements Response.Body { + + private final byte[] data; + + public ByteArrayBody(byte[] data) { + this.data = data; + } + + private static Body orNull(byte[] data) { + if (data == null) { + return null; + } + return new ByteArrayBody(data); + } + + private static Body orNull(String text, Charset charset) { + if (text == null) { + return null; + } + checkNotNull(charset, "charset"); + return new ByteArrayBody(text.getBytes(charset)); } @Override - public void close() { - Util.ensureClosed(body); + public Integer length() { + return data.length; } - public interface Body extends Closeable { - - /** - * length in bytes, if known. Null if unknown or greater than {@link Integer#MAX_VALUE}. - *

- *
- *
- *
- * Note
- * This is an integer as most implementations cannot do bodies greater than 2GB. - */ - Integer length(); - - /** - * 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; - - /** - * for custom charset - */ - Reader asReader(Charset charset) throws IOException; + @Override + public boolean isRepeatable() { + return true; } - private static final class InputStreamBody implements Response.Body { - - private final InputStream inputStream; - private final Integer length; - - private InputStreamBody(InputStream inputStream, Integer length) { - this.inputStream = inputStream; - this.length = length; - } - - private static Body orNull(InputStream inputStream, Integer length) { - if (inputStream == null) { - return null; - } - return new InputStreamBody(inputStream, length); - } - - @Override - public Integer length() { - return length; - } - - @Override - public boolean isRepeatable() { - return false; - } - - @Override - public InputStream asInputStream() throws IOException { - return inputStream; - } - - @Override - public Reader asReader() throws IOException { - return asReader(UTF_8); - } - - public Reader asReader(Charset charset) throws IOException { - return new InputStreamReader(asInputStream(), charset == null ? UTF_8 : charset); - } - - @Override - public void close() throws IOException { - inputStream.close(); - } + @Override + public InputStream asInputStream() throws IOException { + return new ByteArrayInputStream(data); } - private static final class ByteArrayBody implements Response.Body { - - private final byte[] data; - - public ByteArrayBody(byte[] data) { - this.data = data; - } - - private static Body orNull(byte[] data) { - if (data == null) { - return null; - } - return new ByteArrayBody(data); - } - - private static Body orNull(String text, Charset charset) { - if (text == null) { - return null; - } - checkNotNull(charset, "charset"); - return new ByteArrayBody(text.getBytes(charset)); - } - - @Override - public Integer 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 InputStreamReader(asInputStream(), UTF_8); - } - - public Reader asReader(Charset charset) throws IOException { - return new InputStreamReader(asInputStream(), charset == null ? UTF_8 : charset); - } - - @Override - public void close() throws IOException { - } - - @Override - public String toString() { - return decodeOrDefault(data, UTF_8, "Binary data"); - } + @Override + public Reader asReader() throws IOException { + return new InputStreamReader(asInputStream(), UTF_8); + } + + public Reader asReader(Charset charset) throws IOException { + return new InputStreamReader(asInputStream(), charset == null ? UTF_8 : charset); } - private static Map> caseInsensitiveCopyOf(Map> headers) { - Map> result = - new TreeMap>(String.CASE_INSENSITIVE_ORDER); - - for (Map.Entry> entry : headers.entrySet()) { - String headerName = entry.getKey(); - if (!result.containsKey(headerName)) { - result.put(headerName.toLowerCase(Locale.ROOT), new LinkedList()); - } - result.get(headerName).addAll(entry.getValue()); - } - return result; + @Override + public void close() throws IOException {} + + @Override + public String toString() { + return decodeOrDefault(data, UTF_8, "Binary data"); + } + } + + private static Map> caseInsensitiveCopyOf(Map> headers) { + Map> result = + new TreeMap>(String.CASE_INSENSITIVE_ORDER); + + for (Map.Entry> entry : headers.entrySet()) { + String headerName = entry.getKey(); + if (!result.containsKey(headerName)) { + result.put(headerName.toLowerCase(Locale.ROOT), new LinkedList()); + } + result.get(headerName).addAll(entry.getValue()); } + return result; + } } diff --git a/core/src/test/java/feign/FeignBuilderTest.java b/core/src/test/java/feign/FeignBuilderTest.java index a69b4c681e..1b04d39a17 100644 --- a/core/src/test/java/feign/FeignBuilderTest.java +++ b/core/src/test/java/feign/FeignBuilderTest.java @@ -13,32 +13,36 @@ */ package feign; -import java.util.HashMap; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import org.junit.Rule; -import org.junit.Test; +import static feign.assertj.MockWebServerAssertions.assertThat; +import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Type; +import java.nio.charset.Charset; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Rule; +import org.junit.Test; + import feign.codec.Decoder; import feign.codec.Encoder; -import static feign.assertj.MockWebServerAssertions.assertThat; -import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; public class FeignBuilderTest { @@ -399,6 +403,11 @@ public Reader asReader() throws IOException { return original.body().asReader(); } + @Override + public Reader asReader(Charset charset) throws IOException { + return original.body().asReader(charset); + } + @Override public void close() throws IOException { closed.set(true); diff --git a/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java b/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java index 5139af46a7..bc165235ab 100644 --- a/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java +++ b/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java @@ -13,6 +13,21 @@ */ package feign.httpclient; +import static feign.Util.UTF_8; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; @@ -30,29 +45,11 @@ import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.util.EntityUtils; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.io.UnsupportedEncodingException; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - import feign.Client; import feign.Request; import feign.Response; import feign.Util; -import static feign.Util.UTF_8; - /** * This module directs Feign's http requests to Apache's * HttpClient. Ex. @@ -65,178 +62,178 @@ * Based on Square, Inc's Retrofit ApacheClient implementation */ public final class ApacheHttpClient implements Client { - private static final String ACCEPT_HEADER_NAME = "Accept"; + private static final String ACCEPT_HEADER_NAME = "Accept"; - private final HttpClient client; + private final HttpClient client; - public ApacheHttpClient() { - this(HttpClientBuilder.create().build()); - } + public ApacheHttpClient() { + this(HttpClientBuilder.create().build()); + } - public ApacheHttpClient(HttpClient client) { - this.client = client; - } + public ApacheHttpClient(HttpClient client) { + this.client = client; + } - @Override - public Response execute(Request request, Request.Options options) throws IOException { - HttpUriRequest httpUriRequest; - try { - httpUriRequest = toHttpUriRequest(request, options); - } catch (URISyntaxException e) { - throw new IOException("URL '" + request.url() + "' couldn't be parsed into a URI", e); - } - HttpResponse httpResponse = client.execute(httpUriRequest); - return toFeignResponse(httpResponse, request); + @Override + public Response execute(Request request, Request.Options options) throws IOException { + HttpUriRequest httpUriRequest; + try { + httpUriRequest = toHttpUriRequest(request, options); + } catch (URISyntaxException e) { + throw new IOException("URL '" + request.url() + "' couldn't be parsed into a URI", e); } + HttpResponse httpResponse = client.execute(httpUriRequest); + return toFeignResponse(httpResponse, request); + } + + HttpUriRequest toHttpUriRequest(Request request, Request.Options options) + throws UnsupportedEncodingException, MalformedURLException, URISyntaxException { + RequestBuilder requestBuilder = RequestBuilder.create(request.method()); + + // per request timeouts + RequestConfig requestConfig = + (client instanceof Configurable ? RequestConfig.copy(((Configurable) client).getConfig()) + : RequestConfig.custom()) + .setConnectTimeout(options.connectTimeoutMillis()) + .setSocketTimeout(options.readTimeoutMillis()) + .build(); + requestBuilder.setConfig(requestConfig); - HttpUriRequest toHttpUriRequest(Request request, Request.Options options) - throws UnsupportedEncodingException, MalformedURLException, URISyntaxException { - RequestBuilder requestBuilder = RequestBuilder.create(request.method()); - - // per request timeouts - RequestConfig requestConfig = - (client instanceof Configurable ? RequestConfig.copy(((Configurable) client).getConfig()) - : RequestConfig.custom()) - .setConnectTimeout(options.connectTimeoutMillis()) - .setSocketTimeout(options.readTimeoutMillis()) - .build(); - requestBuilder.setConfig(requestConfig); - - URI uri = new URIBuilder(request.url()).build(); - - requestBuilder.setUri(uri.getScheme() + "://" + uri.getAuthority() + uri.getRawPath()); - - // request query params - List queryParams = - URLEncodedUtils.parse(uri, requestBuilder.getCharset().name()); - for (NameValuePair queryParam : queryParams) { - requestBuilder.addParameter(queryParam); - } + URI uri = new URIBuilder(request.url()).build(); - // request headers - boolean hasAcceptHeader = false; - for (Map.Entry> headerEntry : request.headers().entrySet()) { - String headerName = headerEntry.getKey(); - if (headerName.equalsIgnoreCase(ACCEPT_HEADER_NAME)) { - hasAcceptHeader = true; - } - - if (headerName.equalsIgnoreCase(Util.CONTENT_LENGTH)) { - // The 'Content-Length' header is always set by the Apache client and it - // doesn't like us to set it as well. - continue; - } - - for (String headerValue : headerEntry.getValue()) { - requestBuilder.addHeader(headerName, headerValue); - } - } - // some servers choke on the default accept string, so we'll set it to anything - if (!hasAcceptHeader) { - requestBuilder.addHeader(ACCEPT_HEADER_NAME, "*/*"); - } + requestBuilder.setUri(uri.getScheme() + "://" + uri.getAuthority() + uri.getRawPath()); - // request body - if (request.body() != null) { - HttpEntity entity = null; - if (request.charset() != null) { - ContentType contentType = getContentType(request); - String content = new String(request.body(), request.charset()); - entity = new StringEntity(content, contentType); - } else { - entity = new ByteArrayEntity(request.body()); - } - - requestBuilder.setEntity(entity); - } else { - requestBuilder.setEntity(new ByteArrayEntity(new byte[0])); - } - - return requestBuilder.build(); + // request query params + List queryParams = + URLEncodedUtils.parse(uri, requestBuilder.getCharset().name()); + for (NameValuePair queryParam : queryParams) { + requestBuilder.addParameter(queryParam); } - private ContentType getContentType(Request request) { - ContentType contentType = ContentType.DEFAULT_TEXT; - for (Map.Entry> entry : request.headers().entrySet()) - if (entry.getKey().equalsIgnoreCase("Content-Type")) { - Collection values = entry.getValue(); - if (values != null && !values.isEmpty()) { - contentType = ContentType.parse(values.iterator().next()); - if (contentType.getCharset() == null) { - contentType = contentType.withCharset(request.charset()); - } - break; - } - } - return contentType; + // request headers + boolean hasAcceptHeader = false; + for (Map.Entry> headerEntry : request.headers().entrySet()) { + String headerName = headerEntry.getKey(); + if (headerName.equalsIgnoreCase(ACCEPT_HEADER_NAME)) { + hasAcceptHeader = true; + } + + if (headerName.equalsIgnoreCase(Util.CONTENT_LENGTH)) { + // The 'Content-Length' header is always set by the Apache client and it + // doesn't like us to set it as well. + continue; + } + + for (String headerValue : headerEntry.getValue()) { + requestBuilder.addHeader(headerName, headerValue); + } + } + // some servers choke on the default accept string, so we'll set it to anything + if (!hasAcceptHeader) { + requestBuilder.addHeader(ACCEPT_HEADER_NAME, "*/*"); } - Response toFeignResponse(HttpResponse httpResponse, Request request) throws IOException { - StatusLine statusLine = httpResponse.getStatusLine(); - int statusCode = statusLine.getStatusCode(); - - String reason = statusLine.getReasonPhrase(); - - Map> headers = new HashMap>(); - for (Header header : httpResponse.getAllHeaders()) { - String name = header.getName(); - String value = header.getValue(); + // request body + if (request.body() != null) { + HttpEntity entity = null; + if (request.charset() != null) { + ContentType contentType = getContentType(request); + String content = new String(request.body(), request.charset()); + entity = new StringEntity(content, contentType); + } else { + entity = new ByteArrayEntity(request.body()); + } + + requestBuilder.setEntity(entity); + } else { + requestBuilder.setEntity(new ByteArrayEntity(new byte[0])); + } - Collection headerValues = headers.get(name); - if (headerValues == null) { - headerValues = new ArrayList(); - headers.put(name, headerValues); - } - headerValues.add(value); + return requestBuilder.build(); + } + + private ContentType getContentType(Request request) { + ContentType contentType = ContentType.DEFAULT_TEXT; + for (Map.Entry> entry : request.headers().entrySet()) + if (entry.getKey().equalsIgnoreCase("Content-Type")) { + Collection values = entry.getValue(); + if (values != null && !values.isEmpty()) { + contentType = ContentType.parse(values.iterator().next()); + if (contentType.getCharset() == null) { + contentType = contentType.withCharset(request.charset()); + } + break; } - - return Response.builder() - .status(statusCode) - .reason(reason) - .headers(headers) - .request(request) - .body(toFeignBody(httpResponse)) - .build(); + } + return contentType; + } + + Response toFeignResponse(HttpResponse httpResponse, Request request) throws IOException { + StatusLine statusLine = httpResponse.getStatusLine(); + int statusCode = statusLine.getStatusCode(); + + String reason = statusLine.getReasonPhrase(); + + Map> headers = new HashMap>(); + for (Header header : httpResponse.getAllHeaders()) { + String name = header.getName(); + String value = header.getValue(); + + Collection headerValues = headers.get(name); + if (headerValues == null) { + headerValues = new ArrayList(); + headers.put(name, headerValues); + } + headerValues.add(value); } - Response.Body toFeignBody(HttpResponse httpResponse) throws IOException { - final HttpEntity entity = httpResponse.getEntity(); - if (entity == null) { - return null; - } - return new Response.Body() { - - @Override - public Integer length() { - return entity.getContentLength() >= 0 && entity.getContentLength() <= Integer.MAX_VALUE - ? (int) entity.getContentLength() - : null; - } - - @Override - public boolean isRepeatable() { - return entity.isRepeatable(); - } - - @Override - public InputStream asInputStream() throws IOException { - return entity.getContent(); - } - - @Override - public Reader asReader() throws IOException { - return new InputStreamReader(asInputStream(), UTF_8); - } - - @Override - public Reader asReader(Charset charset) throws IOException { - return new InputStreamReader(asInputStream(), charset == null ? UTF_8 : charset); - } - - @Override - public void close() throws IOException { - EntityUtils.consume(entity); - } - }; + return Response.builder() + .status(statusCode) + .reason(reason) + .headers(headers) + .request(request) + .body(toFeignBody(httpResponse)) + .build(); + } + + Response.Body toFeignBody(HttpResponse httpResponse) throws IOException { + final HttpEntity entity = httpResponse.getEntity(); + if (entity == null) { + return null; } + return new Response.Body() { + + @Override + public Integer length() { + return entity.getContentLength() >= 0 && entity.getContentLength() <= Integer.MAX_VALUE + ? (int) entity.getContentLength() + : null; + } + + @Override + public boolean isRepeatable() { + return entity.isRepeatable(); + } + + @Override + public InputStream asInputStream() throws IOException { + return entity.getContent(); + } + + @Override + public Reader asReader() throws IOException { + return new InputStreamReader(asInputStream(), UTF_8); + } + + @Override + public Reader asReader(Charset charset) throws IOException { + return new InputStreamReader(asInputStream(), charset == null ? UTF_8 : charset); + } + + @Override + public void close() throws IOException { + EntityUtils.consume(entity); + } + }; + } } diff --git a/okhttp/src/main/java/feign/okhttp/OkHttpClient.java b/okhttp/src/main/java/feign/okhttp/OkHttpClient.java index bbe4670e1c..9f11f49aa7 100644 --- a/okhttp/src/main/java/feign/okhttp/OkHttpClient.java +++ b/okhttp/src/main/java/feign/okhttp/OkHttpClient.java @@ -13,12 +13,6 @@ */ package feign.okhttp; -import okhttp3.Headers; -import okhttp3.MediaType; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.ResponseBody; import java.io.IOException; import java.io.InputStream; import java.io.Reader; @@ -27,6 +21,12 @@ import java.util.Map; import java.util.concurrent.TimeUnit; import feign.Client; +import okhttp3.Headers; +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; /** * This module directs Feign's http requests to From 816fa6899737794cae04f5708fce85de6b042bf9 Mon Sep 17 00:00:00 2001 From: shiya Date: Mon, 13 Aug 2018 14:02:55 +0800 Subject: [PATCH 3/7] support charset when build Reader for Response.Body --- core/src/main/java/feign/Response.java | 565 +++++++++--------- .../feign/httpclient/ApacheHttpClient.java | 5 +- 2 files changed, 289 insertions(+), 281 deletions(-) diff --git a/core/src/main/java/feign/Response.java b/core/src/main/java/feign/Response.java index afb76f177a..378d50e40c 100644 --- a/core/src/main/java/feign/Response.java +++ b/core/src/main/java/feign/Response.java @@ -18,6 +18,7 @@ import static feign.Util.checkState; import static feign.Util.decodeOrDefault; import static feign.Util.valuesOrEmpty; + import java.io.ByteArrayInputStream; import java.io.Closeable; import java.io.IOException; @@ -37,325 +38,329 @@ */ public final class Response implements Closeable { - private final int status; - private final String reason; - private final Map> headers; - private final Body body; - private final Request request; - - private Response(Builder builder) { - checkState(builder.status >= 200, "Invalid status code: %s", builder.status); - checkState(builder.request != null, "original request is required"); - this.status = builder.status; - this.request = builder.request; - this.reason = builder.reason; // nullable - this.headers = Collections.unmodifiableMap(caseInsensitiveCopyOf(builder.headers)); - this.body = builder.body; // nullable - } - - public Builder toBuilder() { - return new Builder(this); - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - int status; - String reason; - Map> headers; - Body body; - Request request; - - Builder() {} - - Builder(Response source) { - this.status = source.status; - this.reason = source.reason; - this.headers = source.headers; - this.body = source.body; - this.request = source.request; + private final int status; + private final String reason; + private final Map> headers; + private final Body body; + private final Request request; + + private Response(Builder builder) { + checkState(builder.status >= 200, "Invalid status code: %s", builder.status); + checkState(builder.request != null, "original request is required"); + this.status = builder.status; + this.request = builder.request; + this.reason = builder.reason; // nullable + this.headers = Collections.unmodifiableMap(caseInsensitiveCopyOf(builder.headers)); + this.body = builder.body; // nullable } - /** - * @see Response#status - */ - public Builder status(int status) { - this.status = status; - return this; + public Builder toBuilder() { + return new Builder(this); } - /** - * @see Response#reason - */ - public Builder reason(String reason) { - this.reason = reason; - return this; + public static Builder builder() { + return new Builder(); } - /** - * @see Response#headers - */ - public Builder headers(Map> headers) { - this.headers = headers; - return this; + public static final class Builder { + int status; + String reason; + Map> headers; + Body body; + Request request; + + Builder() { + } + + Builder(Response source) { + this.status = source.status; + this.reason = source.reason; + this.headers = source.headers; + this.body = source.body; + this.request = source.request; + } + + /** + * @see Response#status + */ + public Builder status(int status) { + this.status = status; + return this; + } + + /** + * @see Response#reason + */ + public Builder reason(String reason) { + this.reason = reason; + return this; + } + + /** + * @see Response#headers + */ + public Builder headers(Map> headers) { + this.headers = headers; + return this; + } + + /** + * @see Response#body + */ + public Builder body(Body body) { + this.body = body; + return this; + } + + /** + * @see Response#body + */ + public Builder body(InputStream inputStream, Integer length) { + this.body = InputStreamBody.orNull(inputStream, length); + return this; + } + + /** + * @see Response#body + */ + public Builder body(byte[] data) { + this.body = ByteArrayBody.orNull(data); + return this; + } + + /** + * @see Response#body + */ + public Builder body(String text, Charset charset) { + this.body = ByteArrayBody.orNull(text, charset); + return this; + } + + /** + * @see Response#request + */ + public Builder request(Request request) { + checkNotNull(request, "request is required"); + this.request = request; + return this; + } + + public Response build() { + return new Response(this); + } } /** - * @see Response#body + * status code. ex {@code 200} + *

+ * See rfc2616 */ - public Builder body(Body body) { - this.body = body; - return this; + public int status() { + return status; } /** - * @see Response#body + * Nullable and not set when using http/2 + *

+ * See https://github.com/http2/http2-spec/issues/202 */ - public Builder body(InputStream inputStream, Integer length) { - this.body = InputStreamBody.orNull(inputStream, length); - return this; + public String reason() { + return reason; } /** - * @see Response#body + * Returns a case-insensitive mapping of header names to their values. */ - public Builder body(byte[] data) { - this.body = ByteArrayBody.orNull(data); - return this; + public Map> headers() { + return headers; } /** - * @see Response#body + * if present, the response had a body */ - public Builder body(String text, Charset charset) { - this.body = ByteArrayBody.orNull(text, charset); - return this; + public Body body() { + return body; } /** - * @see Response#request + * the request that generated this response */ - public Builder request(Request request) { - checkNotNull(request, "request is required"); - this.request = request; - return this; - } - - public Response build() { - return new Response(this); - } - } - - /** - * status code. ex {@code 200} - *

- * See rfc2616 - */ - public int status() { - return status; - } - - /** - * Nullable and not set when using http/2 - *

- * See https://github.com/http2/http2-spec/issues/202 - */ - public String reason() { - return reason; - } - - /** - * Returns a case-insensitive mapping of header names to their values. - */ - public Map> headers() { - return headers; - } - - /** - * if present, the response had a body - */ - public Body body() { - return body; - } - - /** - * the request that generated this response - */ - public Request request() { - return request; - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder("HTTP/1.1 ").append(status); - if (reason != null) - builder.append(' ').append(reason); - builder.append('\n'); - for (String field : headers.keySet()) { - for (String value : valuesOrEmpty(headers, field)) { - builder.append(field).append(": ").append(value).append('\n'); - } - } - if (body != null) - builder.append('\n').append(body); - return builder.toString(); - } - - @Override - public void close() { - Util.ensureClosed(body); - } - - public interface Body extends Closeable { - - /** - * length in bytes, if known. Null if unknown or greater than {@link Integer#MAX_VALUE}. - *

- *
- *
- *
- * Note
- * This is an integer as most implementations cannot do bodies greater than 2GB. - */ - Integer length(); - - /** - * 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; - - /** - * for custom charset - */ - Reader asReader(Charset charset) throws IOException; - } - - private static final class InputStreamBody implements Response.Body { - - private final InputStream inputStream; - private final Integer length; - - private InputStreamBody(InputStream inputStream, Integer length) { - this.inputStream = inputStream; - this.length = length; - } - - private static Body orNull(InputStream inputStream, Integer length) { - if (inputStream == null) { - return null; - } - return new InputStreamBody(inputStream, length); - } - - @Override - public Integer length() { - return length; - } - - @Override - public boolean isRepeatable() { - return false; - } - - @Override - public InputStream asInputStream() throws IOException { - return inputStream; - } - - @Override - public Reader asReader() throws IOException { - return asReader(UTF_8); - } - - public Reader asReader(Charset charset) throws IOException { - return new InputStreamReader(asInputStream(), charset == null ? UTF_8 : charset); - } - - @Override - public void close() throws IOException { - inputStream.close(); - } - } - - private static final class ByteArrayBody implements Response.Body { - - private final byte[] data; - - public ByteArrayBody(byte[] data) { - this.data = data; - } - - private static Body orNull(byte[] data) { - if (data == null) { - return null; - } - return new ByteArrayBody(data); - } - - private static Body orNull(String text, Charset charset) { - if (text == null) { - return null; - } - checkNotNull(charset, "charset"); - return new ByteArrayBody(text.getBytes(charset)); + public Request request() { + return request; } @Override - public Integer length() { - return data.length; + public String toString() { + StringBuilder builder = new StringBuilder("HTTP/1.1 ").append(status); + if (reason != null) + builder.append(' ').append(reason); + builder.append('\n'); + for (String field : headers.keySet()) { + for (String value : valuesOrEmpty(headers, field)) { + builder.append(field).append(": ").append(value).append('\n'); + } + } + if (body != null) + builder.append('\n').append(body); + return builder.toString(); } @Override - public boolean isRepeatable() { - return true; + public void close() { + Util.ensureClosed(body); } - @Override - public InputStream asInputStream() throws IOException { - return new ByteArrayInputStream(data); + public interface Body extends Closeable { + + /** + * length in bytes, if known. Null if unknown or greater than {@link Integer#MAX_VALUE}. + *

+ *
+ *
+ *
+ * Note
+ * This is an integer as most implementations cannot do bodies greater than 2GB. + */ + Integer length(); + + /** + * 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; + + /** + * for custom charset + */ + Reader asReader(Charset charset) throws IOException; } - @Override - public Reader asReader() throws IOException { - return new InputStreamReader(asInputStream(), UTF_8); + private static final class InputStreamBody implements Response.Body { + + private final InputStream inputStream; + private final Integer length; + + private InputStreamBody(InputStream inputStream, Integer length) { + this.inputStream = inputStream; + this.length = length; + } + + private static Body orNull(InputStream inputStream, Integer length) { + if (inputStream == null) { + return null; + } + return new InputStreamBody(inputStream, length); + } + + @Override + public Integer length() { + return length; + } + + @Override + public boolean isRepeatable() { + return false; + } + + @Override + public InputStream asInputStream() throws IOException { + return inputStream; + } + + @Override + public Reader asReader() throws IOException { + return asReader(UTF_8); + } + + public Reader asReader(Charset charset) throws IOException { + checkNotNull(charset, "charset should not be null"); + return new InputStreamReader(asInputStream(), charset); + } + + @Override + public void close() throws IOException { + inputStream.close(); + } } - public Reader asReader(Charset charset) throws IOException { - return new InputStreamReader(asInputStream(), charset == null ? UTF_8 : charset); + private static final class ByteArrayBody implements Response.Body { + + private final byte[] data; + + public ByteArrayBody(byte[] data) { + this.data = data; + } + + private static Body orNull(byte[] data) { + if (data == null) { + return null; + } + return new ByteArrayBody(data); + } + + private static Body orNull(String text, Charset charset) { + if (text == null) { + return null; + } + checkNotNull(charset, "charset"); + return new ByteArrayBody(text.getBytes(charset)); + } + + @Override + public Integer 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 InputStreamReader(asInputStream(), UTF_8); + } + + public Reader asReader(Charset charset) throws IOException { + checkNotNull(charset, "charset should not be null"); + return new InputStreamReader(asInputStream(), charset); + } + + @Override + public void close() throws IOException { + } + + @Override + public String toString() { + return decodeOrDefault(data, UTF_8, "Binary data"); + } } - @Override - public void close() throws IOException {} - - @Override - public String toString() { - return decodeOrDefault(data, UTF_8, "Binary data"); - } - } - - private static Map> caseInsensitiveCopyOf(Map> headers) { - Map> result = - new TreeMap>(String.CASE_INSENSITIVE_ORDER); - - for (Map.Entry> entry : headers.entrySet()) { - String headerName = entry.getKey(); - if (!result.containsKey(headerName)) { - result.put(headerName.toLowerCase(Locale.ROOT), new LinkedList()); - } - result.get(headerName).addAll(entry.getValue()); + private static Map> caseInsensitiveCopyOf(Map> headers) { + Map> result = + new TreeMap>(String.CASE_INSENSITIVE_ORDER); + + for (Map.Entry> entry : headers.entrySet()) { + String headerName = entry.getKey(); + if (!result.containsKey(headerName)) { + result.put(headerName.toLowerCase(Locale.ROOT), new LinkedList()); + } + result.get(headerName).addAll(entry.getValue()); + } + return result; } - return result; - } } diff --git a/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java b/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java index bc165235ab..fafddbf5a8 100644 --- a/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java +++ b/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java @@ -14,6 +14,8 @@ package feign.httpclient; import static feign.Util.UTF_8; +import static feign.Util.checkNotNull; + import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -227,7 +229,8 @@ public Reader asReader() throws IOException { @Override public Reader asReader(Charset charset) throws IOException { - return new InputStreamReader(asInputStream(), charset == null ? UTF_8 : charset); + checkNotNull(charset, "charset should not be null"); + return new InputStreamReader(asInputStream(), charset); } @Override From 66741137e1585fa4c58192d25ced2c655591b5af Mon Sep 17 00:00:00 2001 From: shiya Date: Mon, 13 Aug 2018 14:25:02 +0800 Subject: [PATCH 4/7] support charset when build Reader for Response.Body --- core/src/main/java/feign/Response.java | 59 +++++++------------ .../src/test/java/feign/FeignBuilderTest.java | 27 ++++----- .../feign/httpclient/ApacheHttpClient.java | 34 +++++------ .../main/java/feign/okhttp/OkHttpClient.java | 12 ++-- 4 files changed, 56 insertions(+), 76 deletions(-) diff --git a/core/src/main/java/feign/Response.java b/core/src/main/java/feign/Response.java index 378d50e40c..31e0374c96 100644 --- a/core/src/main/java/feign/Response.java +++ b/core/src/main/java/feign/Response.java @@ -13,12 +13,6 @@ */ package feign; -import static feign.Util.UTF_8; -import static feign.Util.checkNotNull; -import static feign.Util.checkState; -import static feign.Util.decodeOrDefault; -import static feign.Util.valuesOrEmpty; - import java.io.ByteArrayInputStream; import java.io.Closeable; import java.io.IOException; @@ -32,6 +26,11 @@ import java.util.Locale; import java.util.Map; import java.util.TreeMap; +import static feign.Util.UTF_8; +import static feign.Util.checkNotNull; +import static feign.Util.checkState; +import static feign.Util.decodeOrDefault; +import static feign.Util.valuesOrEmpty; /** * An immutable response to an http invocation which only returns string content. @@ -69,8 +68,7 @@ public static final class Builder { Body body; Request request; - Builder() { - } + Builder() {} Builder(Response source) { this.status = source.status; @@ -80,57 +78,43 @@ public static final class Builder { this.request = source.request; } - /** - * @see Response#status - */ + /** @see Response#status */ public Builder status(int status) { this.status = status; return this; } - /** - * @see Response#reason - */ + /** @see Response#reason */ public Builder reason(String reason) { this.reason = reason; return this; } - /** - * @see Response#headers - */ + /** @see Response#headers */ public Builder headers(Map> headers) { this.headers = headers; return this; } - /** - * @see Response#body - */ + /** @see Response#body */ public Builder body(Body body) { this.body = body; return this; } - /** - * @see Response#body - */ + /** @see Response#body */ public Builder body(InputStream inputStream, Integer length) { this.body = InputStreamBody.orNull(inputStream, length); return this; } - /** - * @see Response#body - */ + /** @see Response#body */ public Builder body(byte[] data) { this.body = ByteArrayBody.orNull(data); return this; } - /** - * @see Response#body - */ + /** @see Response#body */ public Builder body(String text, Charset charset) { this.body = ByteArrayBody.orNull(text, charset); return this; @@ -152,7 +136,7 @@ public Response build() { /** * status code. ex {@code 200} - *

+ * * See rfc2616 */ public int status() { @@ -161,7 +145,7 @@ public int status() { /** * Nullable and not set when using http/2 - *

+ * * See https://github.com/http2/http2-spec/issues/202 */ public String reason() { @@ -214,7 +198,7 @@ public interface Body extends Closeable { /** * length in bytes, if known. Null if unknown or greater than {@link Integer#MAX_VALUE}. - *

+ * *
*
*
@@ -239,7 +223,7 @@ public interface Body extends Closeable { Reader asReader() throws IOException; /** - * for custom charset + * */ Reader asReader(Charset charset) throws IOException; } @@ -278,12 +262,13 @@ public InputStream asInputStream() throws IOException { @Override public Reader asReader() throws IOException { - return asReader(UTF_8); + return new InputStreamReader(inputStream, UTF_8); } + @Override public Reader asReader(Charset charset) throws IOException { checkNotNull(charset, "charset should not be null"); - return new InputStreamReader(asInputStream(), charset); + return new InputStreamReader(inputStream, charset); } @Override @@ -335,14 +320,14 @@ public Reader asReader() throws IOException { return new InputStreamReader(asInputStream(), UTF_8); } + @Override public Reader asReader(Charset charset) throws IOException { checkNotNull(charset, "charset should not be null"); return new InputStreamReader(asInputStream(), charset); } @Override - public void close() throws IOException { - } + public void close() throws IOException {} @Override public String toString() { diff --git a/core/src/test/java/feign/FeignBuilderTest.java b/core/src/test/java/feign/FeignBuilderTest.java index 1b04d39a17..74694b9301 100644 --- a/core/src/test/java/feign/FeignBuilderTest.java +++ b/core/src/test/java/feign/FeignBuilderTest.java @@ -13,36 +13,33 @@ */ package feign; -import static feign.assertj.MockWebServerAssertions.assertThat; -import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - +import java.util.HashMap; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.Rule; +import org.junit.Test; import java.io.IOException; import java.io.InputStream; import java.io.Reader; +import java.nio.charset.Charset; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Type; -import java.nio.charset.Charset; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; - -import org.junit.Rule; -import org.junit.Test; - import feign.codec.Decoder; import feign.codec.Encoder; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; +import static feign.assertj.MockWebServerAssertions.assertThat; +import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; public class FeignBuilderTest { diff --git a/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java b/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java index fafddbf5a8..9ed32914b3 100644 --- a/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java +++ b/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java @@ -13,23 +13,6 @@ */ package feign.httpclient; -import static feign.Util.UTF_8; -import static feign.Util.checkNotNull; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.io.UnsupportedEncodingException; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; @@ -47,10 +30,25 @@ import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.util.EntityUtils; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.Charset; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import feign.Client; import feign.Request; import feign.Response; import feign.Util; +import static feign.Util.UTF_8; /** * This module directs Feign's http requests to Apache's @@ -229,7 +227,7 @@ public Reader asReader() throws IOException { @Override public Reader asReader(Charset charset) throws IOException { - checkNotNull(charset, "charset should not be null"); + Util.checkNotNull(charset, "charset should not be null"); return new InputStreamReader(asInputStream(), charset); } diff --git a/okhttp/src/main/java/feign/okhttp/OkHttpClient.java b/okhttp/src/main/java/feign/okhttp/OkHttpClient.java index 9f11f49aa7..bbe4670e1c 100644 --- a/okhttp/src/main/java/feign/okhttp/OkHttpClient.java +++ b/okhttp/src/main/java/feign/okhttp/OkHttpClient.java @@ -13,6 +13,12 @@ */ package feign.okhttp; +import okhttp3.Headers; +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; import java.io.IOException; import java.io.InputStream; import java.io.Reader; @@ -21,12 +27,6 @@ import java.util.Map; import java.util.concurrent.TimeUnit; import feign.Client; -import okhttp3.Headers; -import okhttp3.MediaType; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.ResponseBody; /** * This module directs Feign's http requests to From 71a92df90546d4ee8930a254a70f9265163794e0 Mon Sep 17 00:00:00 2001 From: shiya Date: Mon, 13 Aug 2018 14:46:47 +0800 Subject: [PATCH 5/7] support charset when build Reader for Response.Body --- core/src/main/java/feign/Response.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/feign/Response.java b/core/src/main/java/feign/Response.java index 31e0374c96..0f9692563d 100644 --- a/core/src/main/java/feign/Response.java +++ b/core/src/main/java/feign/Response.java @@ -1,11 +1,11 @@ /** * Copyright 2012-2018 The Feign Authors - *

+ * * 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 From 2310cf61c48e38b6a5e6406e4f65657461402d87 Mon Sep 17 00:00:00 2001 From: shiya Date: Mon, 13 Aug 2018 14:55:49 +0800 Subject: [PATCH 6/7] format header --- .../src/main/java/feign/httpclient/ApacheHttpClient.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java b/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java index 9ed32914b3..7919506a98 100644 --- a/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java +++ b/httpclient/src/main/java/feign/httpclient/ApacheHttpClient.java @@ -1,11 +1,11 @@ /** * Copyright 2012-2018 The Feign Authors - *

+ * * 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 From 433d9db60f3788e89a03aba021953187181167e1 Mon Sep 17 00:00:00 2001 From: shiya Date: Tue, 14 Aug 2018 10:34:00 +0800 Subject: [PATCH 7/7] format Response --- core/src/main/java/feign/Response.java | 544 ++++++++++++------------- 1 file changed, 272 insertions(+), 272 deletions(-) diff --git a/core/src/main/java/feign/Response.java b/core/src/main/java/feign/Response.java index 0f9692563d..46499067a6 100644 --- a/core/src/main/java/feign/Response.java +++ b/core/src/main/java/feign/Response.java @@ -37,315 +37,315 @@ */ public final class Response implements Closeable { - private final int status; - private final String reason; - private final Map> headers; - private final Body body; - private final Request request; - - private Response(Builder builder) { - checkState(builder.status >= 200, "Invalid status code: %s", builder.status); - checkState(builder.request != null, "original request is required"); - this.status = builder.status; - this.request = builder.request; - this.reason = builder.reason; // nullable - this.headers = Collections.unmodifiableMap(caseInsensitiveCopyOf(builder.headers)); - this.body = builder.body; // nullable + private final int status; + private final String reason; + private final Map> headers; + private final Body body; + private final Request request; + + private Response(Builder builder) { + checkState(builder.status >= 200, "Invalid status code: %s", builder.status); + checkState(builder.request != null, "original request is required"); + this.status = builder.status; + this.request = builder.request; + this.reason = builder.reason; // nullable + this.headers = Collections.unmodifiableMap(caseInsensitiveCopyOf(builder.headers)); + this.body = builder.body; // nullable + } + + public Builder toBuilder() { + return new Builder(this); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + int status; + String reason; + Map> headers; + Body body; + Request request; + + Builder() {} + + Builder(Response source) { + this.status = source.status; + this.reason = source.reason; + this.headers = source.headers; + this.body = source.body; + this.request = source.request; } - public Builder toBuilder() { - return new Builder(this); + /** @see Response#status */ + public Builder status(int status) { + this.status = status; + return this; } - public static Builder builder() { - return new Builder(); + /** @see Response#reason */ + public Builder reason(String reason) { + this.reason = reason; + return this; } - public static final class Builder { - int status; - String reason; - Map> headers; - Body body; - Request request; - - Builder() {} - - Builder(Response source) { - this.status = source.status; - this.reason = source.reason; - this.headers = source.headers; - this.body = source.body; - this.request = source.request; - } - - /** @see Response#status */ - public Builder status(int status) { - this.status = status; - return this; - } - - /** @see Response#reason */ - public Builder reason(String reason) { - this.reason = reason; - return this; - } - - /** @see Response#headers */ - public Builder headers(Map> headers) { - this.headers = headers; - return this; - } - - /** @see Response#body */ - public Builder body(Body body) { - this.body = body; - return this; - } - - /** @see Response#body */ - public Builder body(InputStream inputStream, Integer length) { - this.body = InputStreamBody.orNull(inputStream, length); - return this; - } - - /** @see Response#body */ - public Builder body(byte[] data) { - this.body = ByteArrayBody.orNull(data); - return this; - } - - /** @see Response#body */ - public Builder body(String text, Charset charset) { - this.body = ByteArrayBody.orNull(text, charset); - return this; - } - - /** - * @see Response#request - */ - public Builder request(Request request) { - checkNotNull(request, "request is required"); - this.request = request; - return this; - } - - public Response build() { - return new Response(this); - } + /** @see Response#headers */ + public Builder headers(Map> headers) { + this.headers = headers; + return this; + } + + /** @see Response#body */ + public Builder body(Body body) { + this.body = body; + return this; + } + + /** @see Response#body */ + public Builder body(InputStream inputStream, Integer length) { + this.body = InputStreamBody.orNull(inputStream, length); + return this; + } + + /** @see Response#body */ + public Builder body(byte[] data) { + this.body = ByteArrayBody.orNull(data); + return this; + } + + /** @see Response#body */ + public Builder body(String text, Charset charset) { + this.body = ByteArrayBody.orNull(text, charset); + return this; } /** - * status code. ex {@code 200} - * - * See rfc2616 + * @see Response#request */ - public int status() { - return status; + public Builder request(Request request) { + checkNotNull(request, "request is required"); + this.request = request; + return this; + } + + public Response build() { + return new Response(this); + } + } + + /** + * status code. ex {@code 200} + * + * See rfc2616 + */ + public int status() { + return status; + } + + /** + * Nullable and not set when using http/2 + * + * See https://github.com/http2/http2-spec/issues/202 + */ + public String reason() { + return reason; + } + + /** + * Returns a case-insensitive mapping of header names to their values. + */ + public Map> headers() { + return headers; + } + + /** + * if present, the response had a body + */ + public Body body() { + return body; + } + + /** + * the request that generated this response + */ + public Request request() { + return request; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("HTTP/1.1 ").append(status); + if (reason != null) + builder.append(' ').append(reason); + builder.append('\n'); + for (String field : headers.keySet()) { + for (String value : valuesOrEmpty(headers, field)) { + builder.append(field).append(": ").append(value).append('\n'); + } } + if (body != null) + builder.append('\n').append(body); + return builder.toString(); + } + + @Override + public void close() { + Util.ensureClosed(body); + } + + public interface Body extends Closeable { /** - * Nullable and not set when using http/2 + * length in bytes, if known. Null if unknown or greater than {@link Integer#MAX_VALUE}. * - * See https://github.com/http2/http2-spec/issues/202 + *
+ *
+ *
+ * Note
+ * This is an integer as most implementations cannot do bodies greater than 2GB. */ - public String reason() { - return reason; - } + Integer length(); /** - * Returns a case-insensitive mapping of header names to their values. + * True if {@link #asInputStream()} and {@link #asReader()} can be called more than once. */ - public Map> headers() { - return headers; - } + boolean isRepeatable(); /** - * if present, the response had a body + * It is the responsibility of the caller to close the stream. */ - public Body body() { - return body; - } + InputStream asInputStream() throws IOException; + + /** + * It is the responsibility of the caller to close the stream. + */ + Reader asReader() throws IOException; /** - * the request that generated this response + * */ - public Request request() { - return request; + Reader asReader(Charset charset) throws IOException; + } + + private static final class InputStreamBody implements Response.Body { + + private final InputStream inputStream; + private final Integer length; + + private InputStreamBody(InputStream inputStream, Integer length) { + this.inputStream = inputStream; + this.length = length; + } + + private static Body orNull(InputStream inputStream, Integer length) { + if (inputStream == null) { + return null; + } + return new InputStreamBody(inputStream, length); } @Override - public String toString() { - StringBuilder builder = new StringBuilder("HTTP/1.1 ").append(status); - if (reason != null) - builder.append(' ').append(reason); - builder.append('\n'); - for (String field : headers.keySet()) { - for (String value : valuesOrEmpty(headers, field)) { - builder.append(field).append(": ").append(value).append('\n'); - } - } - if (body != null) - builder.append('\n').append(body); - return builder.toString(); + public Integer length() { + return length; } @Override - public void close() { - Util.ensureClosed(body); + public boolean isRepeatable() { + return false; } - public interface Body extends Closeable { - - /** - * length in bytes, if known. Null if unknown or greater than {@link Integer#MAX_VALUE}. - * - *
- *
- *
- * Note
- * This is an integer as most implementations cannot do bodies greater than 2GB. - */ - Integer length(); - - /** - * 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; - - /** - * - */ - Reader asReader(Charset charset) throws IOException; + @Override + public InputStream asInputStream() throws IOException { + return inputStream; } - private static final class InputStreamBody implements Response.Body { - - private final InputStream inputStream; - private final Integer length; - - private InputStreamBody(InputStream inputStream, Integer length) { - this.inputStream = inputStream; - this.length = length; - } - - private static Body orNull(InputStream inputStream, Integer length) { - if (inputStream == null) { - return null; - } - return new InputStreamBody(inputStream, length); - } - - @Override - public Integer length() { - return length; - } - - @Override - public boolean isRepeatable() { - return false; - } - - @Override - public InputStream asInputStream() throws IOException { - return inputStream; - } - - @Override - public Reader asReader() throws IOException { - return new InputStreamReader(inputStream, UTF_8); - } - - @Override - public Reader asReader(Charset charset) throws IOException { - checkNotNull(charset, "charset should not be null"); - return new InputStreamReader(inputStream, charset); - } - - @Override - public void close() throws IOException { - inputStream.close(); - } + @Override + public Reader asReader() throws IOException { + return new InputStreamReader(inputStream, UTF_8); } - private static final class ByteArrayBody implements Response.Body { - - private final byte[] data; - - public ByteArrayBody(byte[] data) { - this.data = data; - } - - private static Body orNull(byte[] data) { - if (data == null) { - return null; - } - return new ByteArrayBody(data); - } - - private static Body orNull(String text, Charset charset) { - if (text == null) { - return null; - } - checkNotNull(charset, "charset"); - return new ByteArrayBody(text.getBytes(charset)); - } - - @Override - public Integer 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 InputStreamReader(asInputStream(), UTF_8); - } - - @Override - public Reader asReader(Charset charset) throws IOException { - checkNotNull(charset, "charset should not be null"); - return new InputStreamReader(asInputStream(), charset); - } - - @Override - public void close() throws IOException {} - - @Override - public String toString() { - return decodeOrDefault(data, UTF_8, "Binary data"); - } + @Override + public Reader asReader(Charset charset) throws IOException { + checkNotNull(charset, "charset should not be null"); + return new InputStreamReader(inputStream, charset); } - private static Map> caseInsensitiveCopyOf(Map> headers) { - Map> result = - new TreeMap>(String.CASE_INSENSITIVE_ORDER); - - for (Map.Entry> entry : headers.entrySet()) { - String headerName = entry.getKey(); - if (!result.containsKey(headerName)) { - result.put(headerName.toLowerCase(Locale.ROOT), new LinkedList()); - } - result.get(headerName).addAll(entry.getValue()); - } - return result; + @Override + public void close() throws IOException { + inputStream.close(); + } + } + + private static final class ByteArrayBody implements Response.Body { + + private final byte[] data; + + public ByteArrayBody(byte[] data) { + this.data = data; + } + + private static Body orNull(byte[] data) { + if (data == null) { + return null; + } + return new ByteArrayBody(data); + } + + private static Body orNull(String text, Charset charset) { + if (text == null) { + return null; + } + checkNotNull(charset, "charset"); + return new ByteArrayBody(text.getBytes(charset)); + } + + @Override + public Integer 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 InputStreamReader(asInputStream(), UTF_8); + } + + @Override + public Reader asReader(Charset charset) throws IOException { + checkNotNull(charset, "charset should not be null"); + return new InputStreamReader(asInputStream(), charset); + } + + @Override + public void close() throws IOException {} + + @Override + public String toString() { + return decodeOrDefault(data, UTF_8, "Binary data"); + } + } + + private static Map> caseInsensitiveCopyOf(Map> headers) { + Map> result = + new TreeMap>(String.CASE_INSENSITIVE_ORDER); + + for (Map.Entry> entry : headers.entrySet()) { + String headerName = entry.getKey(); + if (!result.containsKey(headerName)) { + result.put(headerName.toLowerCase(Locale.ROOT), new LinkedList()); + } + result.get(headerName).addAll(entry.getValue()); } + return result; + } }