> headers() {
+ return headers;
+ }
+
+ /**
+ * The character set with which the body is encoded, or null if unknown or not applicable. When
+ * this is present, you can use {@code new String(req.body(), req.charset())} to access the body
+ * as a String.
+ */
+ public Charset charset() {
+ return charset;
+ }
+
+ /**
+ * If present, this is the replayable body to send to the server. In some cases, this may be
+ * interpretable as text.
+ *
+ * @see #charset()
+ */
+ public byte[] body() {
+ return body;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append(method).append(' ').append(url).append(" HTTP/1.1\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(charset != null ? new String(body, charset) : "Binary data");
+ }
+ return builder.toString();
+ }
+
+ /* Controls the per-request settings currently required to be implemented by all {@link Client clients} */
+ public static class Options {
+
+ private final int connectTimeoutMillis;
+ private final int readTimeoutMillis;
+
+ public Options(int connectTimeoutMillis, int readTimeoutMillis) {
+ this.connectTimeoutMillis = connectTimeoutMillis;
+ this.readTimeoutMillis = readTimeoutMillis;
+ }
+
+ public Options() {
+ this(10 * 1000, 60 * 1000);
+ }
+
+ /**
+ * Defaults to 10 seconds. {@code 0} implies no timeout.
+ *
+ * @see java.net.HttpURLConnection#getConnectTimeout()
+ */
+ public int connectTimeoutMillis() {
+ return connectTimeoutMillis;
+ }
+
+ /**
+ * Defaults to 60 seconds. {@code 0} implies no timeout.
+ *
+ * @see java.net.HttpURLConnection#getReadTimeout()
+ */
+ public int readTimeoutMillis() {
+ return readTimeoutMillis;
+ }
+ }
+}
diff --git a/core/src/main/java/feign/RequestInterceptor.java b/core/src/main/java/feign/RequestInterceptor.java
new file mode 100644
index 0000000000..c0864dec6c
--- /dev/null
+++ b/core/src/main/java/feign/RequestInterceptor.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2013 Netflix, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package feign;
+
+/**
+ * Zero or more {@code RequestInterceptors} may be configured for purposes such as adding headers to
+ * all requests. No guarantees are give with regards to the order that interceptors are applied.
+ * Once interceptors are applied, {@link Target#apply(RequestTemplate)} is called to create the
+ * immutable http request sent via {@link Client#execute(Request, feign.Request.Options)}.
+ * For example:
+ *
+ * public void apply(RequestTemplate input) {
+ * input.header("X-Auth", currentToken);
+ * }
+ *
+ * Configuration {@code RequestInterceptors} are configured via {@link
+ * Feign.Builder#requestInterceptors}. Implementation notes Do not add
+ * parameters, such as {@code /path/{foo}/bar } in your implementation of {@link
+ * #apply(RequestTemplate)}. Interceptors are applied after the template's parameters are
+ * {@link RequestTemplate#resolve(java.util.Map) resolved}. This is to ensure that you can
+ * implement signatures are interceptors. Relationship to Retrofit 1.x
+ * This class is similar to {@code RequestInterceptor.intercept()}, except that the implementation
+ * can read, remove, or otherwise mutate any part of the request template.
+ */
+public interface RequestInterceptor {
+
+ /**
+ * Called for every request. Add data using methods on the supplied {@link RequestTemplate}.
+ */
+ void apply(RequestTemplate template);
+}
diff --git a/core/src/main/java/feign/RequestLine.java b/core/src/main/java/feign/RequestLine.java
new file mode 100644
index 0000000000..0666f70cc2
--- /dev/null
+++ b/core/src/main/java/feign/RequestLine.java
@@ -0,0 +1,51 @@
+package feign;
+
+import java.lang.annotation.Retention;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * Expands the request-line supplied in the {@code value}, permitting path and query variables, or
+ * just the http method.
+ *
+ * ...
+ * @RequestLine("POST /servers")
+ * ...
+ *
+ * @RequestLine("GET /servers/{serverId}?count={count}")
+ * void get(@Param("serverId") String serverId, @Param("count") int count);
+ * ...
+ *
+ * @RequestLine("GET")
+ * Response getNext(URI nextLink);
+ * ...
+ *
+ * HTTP version suffix is optional, but permitted. There are no guarantees this version will impact
+ * that sent by the client.
+ *
+ * @RequestLine("POST /servers HTTP/1.1")
+ * ...
+ *
+ * Note: Query params do not overwrite each other. All queries with the same
+ * name will be included in the request. Relationship to JAXRS The following
+ * two forms are identical. Feign:
+ *
+ * @RequestLine("GET /servers/{serverId}?count={count}")
+ * void get(@Param("serverId") String serverId, @Param("count") int count);
+ * ...
+ *
+ * JAX-RS:
+ *
+ * @GET @Path("/servers/{serverId}")
+ * void get(@PathParam("serverId") String serverId, @QueryParam("count") int count);
+ * ...
+ *
+ */
+@java.lang.annotation.Target(METHOD)
+@Retention(RUNTIME)
+public @interface RequestLine {
+
+ String value();
+ boolean decodeSlash() default true;
+}
diff --git a/core/src/main/java/feign/RequestTemplate.java b/core/src/main/java/feign/RequestTemplate.java
new file mode 100644
index 0000000000..5c3616719d
--- /dev/null
+++ b/core/src/main/java/feign/RequestTemplate.java
@@ -0,0 +1,681 @@
+/*
+ * Copyright 2013 Netflix, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package feign;
+
+import java.io.Serializable;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import static feign.Util.CONTENT_LENGTH;
+import static feign.Util.UTF_8;
+import static feign.Util.checkArgument;
+import static feign.Util.checkNotNull;
+import static feign.Util.emptyToNull;
+import static feign.Util.toArray;
+import static feign.Util.valuesOrEmpty;
+
+/**
+ * Builds a request to an http target. Not thread safe. relationship to JAXRS
+ * 2.0 A combination of {@code javax.ws.rs.client.WebTarget} and {@code
+ * javax.ws.rs.client.Invocation.Builder}, ensuring you can modify any part of the request. However,
+ * this object is mutable, so needs to be guarded with the copy constructor.
+ */
+public final class RequestTemplate implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+ private final Map> queries =
+ new LinkedHashMap>();
+ private final Map> headers =
+ new LinkedHashMap>();
+ private String method;
+ /* final to encourage mutable use vs replacing the object. */
+ private StringBuilder url = new StringBuilder();
+ private transient Charset charset;
+ private byte[] body;
+ private String bodyTemplate;
+ private boolean decodeSlash = true;
+
+ public RequestTemplate() {
+ }
+
+ /* Copy constructor. Use this when making templates. */
+ public RequestTemplate(RequestTemplate toCopy) {
+ checkNotNull(toCopy, "toCopy");
+ this.method = toCopy.method;
+ this.url.append(toCopy.url);
+ this.queries.putAll(toCopy.queries);
+ this.headers.putAll(toCopy.headers);
+ this.charset = toCopy.charset;
+ this.body = toCopy.body;
+ this.bodyTemplate = toCopy.bodyTemplate;
+ this.decodeSlash = toCopy.decodeSlash;
+ }
+
+ private static String urlDecode(String arg) {
+ try {
+ return URLDecoder.decode(arg, UTF_8.name());
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ static String urlEncode(Object arg) {
+ try {
+ return URLEncoder.encode(String.valueOf(arg), UTF_8.name());
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static boolean isHttpUrl(CharSequence value) {
+ return value.length() >= 4 && value.subSequence(0, 3).equals("http".substring(0, 3));
+ }
+
+ private static CharSequence removeTrailingSlash(CharSequence charSequence) {
+ if (charSequence != null && charSequence.length() > 0 && charSequence.charAt(charSequence.length() - 1) == '/') {
+ return charSequence.subSequence(0, charSequence.length() - 1);
+ } else {
+ return charSequence;
+ }
+ }
+
+ /**
+ * Expands a {@code template}, such as {@code username}, using the {@code variables} supplied. Any
+ * unresolved parameters will remain. Note that if you'd like curly braces literally in the
+ * {@code template}, urlencode them first.
+ *
+ * @param template URI template that can be in level 1 RFC6570
+ * form.
+ * @param variables to the URI template
+ * @return expanded template, leaving any unresolved parameters literal
+ */
+ public static String expand(String template, Map variables) {
+ // skip expansion if there's no valid variables set. ex. {a} is the
+ // first valid
+ if (checkNotNull(template, "template").length() < 3) {
+ return template;
+ }
+ checkNotNull(variables, "variables for %s", template);
+
+ boolean inVar = false;
+ StringBuilder var = new StringBuilder();
+ StringBuilder builder = new StringBuilder();
+ for (char c : template.toCharArray()) {
+ switch (c) {
+ case '{':
+ if (inVar) {
+ // '{{' is an escape: write the brace and don't interpret as a variable
+ builder.append("{");
+ inVar = false;
+ break;
+ }
+ inVar = true;
+ break;
+ case '}':
+ if (!inVar) { // then write the brace literally
+ builder.append('}');
+ break;
+ }
+ inVar = false;
+ String key = var.toString();
+ Object value = variables.get(var.toString());
+ if (value != null) {
+ builder.append(value);
+ } else {
+ builder.append('{').append(key).append('}');
+ }
+ var = new StringBuilder();
+ break;
+ default:
+ if (inVar) {
+ var.append(c);
+ } else {
+ builder.append(c);
+ }
+ }
+ }
+ return builder.toString();
+ }
+
+ private static Map> parseAndDecodeQueries(String queryLine) {
+ Map> map = new LinkedHashMap>();
+ if (emptyToNull(queryLine) == null) {
+ return map;
+ }
+ if (queryLine.indexOf('&') == -1) {
+ putKV(queryLine, map);
+ } else {
+ char[] chars = queryLine.toCharArray();
+ int start = 0;
+ int i = 0;
+ for (; i < chars.length; i++) {
+ if (chars[i] == '&') {
+ putKV(queryLine.substring(start, i), map);
+ start = i + 1;
+ }
+ }
+ putKV(queryLine.substring(start, i), map);
+ }
+ return map;
+ }
+
+ private static void putKV(String stringToParse, Map> map) {
+ String key;
+ String value;
+ // note that '=' can be a valid part of the value
+ int firstEq = stringToParse.indexOf('=');
+ if (firstEq == -1) {
+ key = urlDecode(stringToParse);
+ value = null;
+ } else {
+ key = urlDecode(stringToParse.substring(0, firstEq));
+ value = urlDecode(stringToParse.substring(firstEq + 1));
+ }
+ Collection values = map.containsKey(key) ? map.get(key) : new ArrayList();
+ values.add(value);
+ map.put(key, values);
+ }
+
+ /** {@link #resolve(Map, Map)}, which assumes no parameter is encoded */
+ public RequestTemplate resolve(Map unencoded) {
+ return resolve(unencoded, Collections.emptyMap());
+ }
+
+ /**
+ * Resolves any template parameters in the requests path, query, or headers against the supplied
+ * unencoded arguments. relationship to JAXRS 2.0 This call is
+ * similar to {@code javax.ws.rs.client.WebTarget.resolveTemplates(templateValues, true)} , except
+ * that the template values apply to any part of the request, not just the URL
+ */
+ RequestTemplate resolve(Map unencoded, Map alreadyEncoded) {
+ replaceQueryValues(unencoded, alreadyEncoded);
+ Map encoded = new LinkedHashMap();
+ for (Entry entry : unencoded.entrySet()) {
+ final String key = entry.getKey();
+ final Object objectValue = entry.getValue();
+ String encodedValue = encodeValueIfNotEncoded(key, objectValue, alreadyEncoded);
+ encoded.put(key, encodedValue);
+ }
+ String resolvedUrl = expand(url.toString(), encoded).replace("+", "%20");
+ if (decodeSlash) {
+ resolvedUrl = resolvedUrl.replace("%2F", "/");
+ }
+ url = new StringBuilder(resolvedUrl);
+
+ Map> resolvedHeaders = new LinkedHashMap>();
+ for (String field : headers.keySet()) {
+ Collection resolvedValues = new ArrayList();
+ for (String value : valuesOrEmpty(headers, field)) {
+ String resolved = expand(value, unencoded);
+ resolvedValues.add(resolved);
+ }
+ resolvedHeaders.put(field, resolvedValues);
+ }
+ headers.clear();
+ headers.putAll(resolvedHeaders);
+ if (bodyTemplate != null) {
+ body(urlDecode(expand(bodyTemplate, encoded)));
+ }
+ return this;
+ }
+
+ private String encodeValueIfNotEncoded(String key, Object objectValue, Map alreadyEncoded) {
+ String value = String.valueOf(objectValue);
+ final Boolean isEncoded = alreadyEncoded.get(key);
+ if (isEncoded == null || !isEncoded) {
+ value = urlEncode(value);
+ }
+ return value;
+ }
+
+ /* roughly analogous to {@code javax.ws.rs.client.Target.request()}. */
+ public Request request() {
+ Map> safeCopy = new LinkedHashMap>();
+ safeCopy.putAll(headers);
+ return Request.create(
+ method, url + queryLine(),
+ Collections.unmodifiableMap(safeCopy),
+ body, charset
+ );
+ }
+
+ /* @see Request#method() */
+ public RequestTemplate method(String method) {
+ this.method = checkNotNull(method, "method");
+ checkArgument(method.matches("^[A-Z]+$"), "Invalid HTTP Method: %s", method);
+ return this;
+ }
+
+ /* @see Request#method() */
+ public String method() {
+ return method;
+ }
+
+ public RequestTemplate decodeSlash(boolean decodeSlash) {
+ this.decodeSlash = decodeSlash;
+ return this;
+ }
+
+ public boolean decodeSlash() {
+ return decodeSlash;
+ }
+
+ /* @see #url() */
+ public RequestTemplate append(CharSequence value) {
+ url.append(value);
+ url = pullAnyQueriesOutOfUrl(url);
+ return this;
+ }
+
+ /* @see #url() */
+ public RequestTemplate insert(int pos, CharSequence value) {
+ if(isHttpUrl(value)) {
+ value = removeTrailingSlash(value);
+ if(url.length() > 0 && url.charAt(0) != '/') {
+ url.insert(0, '/');
+ }
+ }
+ url.insert(pos, pullAnyQueriesOutOfUrl(new StringBuilder(value)));
+ return this;
+ }
+
+ public String url() {
+ return url.toString();
+ }
+
+ /**
+ * Replaces queries with the specified {@code name} with the {@code values} supplied.
+ * Values can be passed in decoded or in url-encoded form depending on the value of the
+ * {@code encoded} parameter.
+ * When the {@code value} is {@code null}, all queries with the {@code configKey} are
+ * removed. relationship to JAXRS 2.0 Like {@code WebTarget.query},
+ * except the values can be templatized. ex.
+ *
+ * template.query("Signature", "{signature}");
+ *
+ * Note: behavior of RequestTemplate is not consistent if a query parameter with
+ * unsafe characters is passed as both encoded and unencoded, although no validation is performed.
+ * ex.
+ *
+ * template.query(true, "param[]", "value");
+ * template.query(false, "param[]", "value");
+ *
+ *
+ * @param encoded whether name and values are already url-encoded
+ * @param name the name of the query
+ * @param values can be a single null to imply removing all values. Else no values are expected
+ * to be null.
+ * @see #queries()
+ */
+ public RequestTemplate query(boolean encoded, String name, String... values) {
+ return doQuery(encoded, name, values);
+ }
+
+ /* @see #query(boolean, String, String...) */
+ public RequestTemplate query(boolean encoded, String name, Iterable values) {
+ return doQuery(encoded, name, values);
+ }
+
+ /**
+ * Shortcut for {@code query(false, String, String...)}
+ * @see #query(boolean, String, String...)
+ */
+ public RequestTemplate query(String name, String... values) {
+ return doQuery(false, name, values);
+ }
+
+ /**
+ * Shortcut for {@code query(false, String, Iterable)}
+ * @see #query(boolean, String, String...)
+ */
+ public RequestTemplate query(String name, Iterable values) {
+ return doQuery(false, name, values);
+ }
+
+ private RequestTemplate doQuery(boolean encoded, String name, String... values) {
+ checkNotNull(name, "name");
+ String paramName = encoded ? name : encodeIfNotVariable(name);
+ queries.remove(paramName);
+ if (values != null && values.length > 0 && values[0] != null) {
+ ArrayList paramValues = new ArrayList();
+ for (String value : values) {
+ paramValues.add(encoded ? value : encodeIfNotVariable(value));
+ }
+ this.queries.put(paramName, paramValues);
+ }
+ return this;
+ }
+
+ private RequestTemplate doQuery(boolean encoded, String name, Iterable values) {
+ if (values != null) {
+ return doQuery(encoded, name, toArray(values, String.class));
+ }
+ return doQuery(encoded, name, (String[]) null);
+ }
+
+ private static String encodeIfNotVariable(String in) {
+ if (in == null || in.indexOf('{') == 0) {
+ return in;
+ }
+ return urlEncode(in);
+ }
+
+ /**
+ * Replaces all existing queries with the newly supplied url decoded queries.
+ * relationship to JAXRS 2.0 Like {@code WebTarget.queries}, except the
+ * values can be templatized. ex.
+ *
+ * template.queries(ImmutableMultimap.of("Signature", "{signature}"));
+ *
+ *
+ * @param queries if null, remove all queries. else value to replace all queries with.
+ * @see #queries()
+ */
+ public RequestTemplate queries(Map> queries) {
+ if (queries == null || queries.isEmpty()) {
+ this.queries.clear();
+ } else {
+ for (Entry> entry : queries.entrySet()) {
+ query(entry.getKey(), toArray(entry.getValue(), String.class));
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Returns an immutable copy of the url decoded queries.
+ *
+ * @see Request#url()
+ */
+ public Map> queries() {
+ Map> decoded = new LinkedHashMap>();
+ for (String field : queries.keySet()) {
+ Collection decodedValues = new ArrayList();
+ for (String value : valuesOrEmpty(queries, field)) {
+ if (value != null) {
+ decodedValues.add(urlDecode(value));
+ } else {
+ decodedValues.add(null);
+ }
+ }
+ decoded.put(urlDecode(field), decodedValues);
+ }
+ return Collections.unmodifiableMap(decoded);
+ }
+
+ /**
+ * Replaces headers with the specified {@code configKey} with the {@code values} supplied.
+ * When the {@code value} is {@code null}, all headers with the {@code configKey} are removed.
+ * relationship to JAXRS 2.0 Like {@code WebTarget.queries} and
+ * {@code javax.ws.rs.client.Invocation.Builder.header}, except the values can be templatized.
+ * ex.
+ *
+ * template.query("X-Application-Version", "{version}");
+ *
+ *
+ * @param name the name of the header
+ * @param values can be a single null to imply removing all values. Else no values are expected to
+ * be null.
+ * @see #headers()
+ */
+ public RequestTemplate header(String name, String... values) {
+ checkNotNull(name, "header name");
+ if (values == null || (values.length == 1 && values[0] == null)) {
+ headers.remove(name);
+ } else {
+ List headers = new ArrayList();
+ headers.addAll(Arrays.asList(values));
+ this.headers.put(name, headers);
+ }
+ return this;
+ }
+
+ /* @see #header(String, String...) */
+ public RequestTemplate header(String name, Iterable values) {
+ if (values != null) {
+ return header(name, toArray(values, String.class));
+ }
+ return header(name, (String[]) null);
+ }
+
+ /**
+ * Replaces all existing headers with the newly supplied headers. relationship to
+ * JAXRS 2.0 Like {@code Invocation.Builder.headers(MultivaluedMap)}, except the
+ * values can be templatized. ex.
+ *
+ * template.headers(mapOf("X-Application-Version", asList("{version}")));
+ *
+ *
+ * @param headers if null, remove all headers. else value to replace all headers with.
+ * @see #headers()
+ */
+ public RequestTemplate headers(Map> headers) {
+ if (headers == null || headers.isEmpty()) {
+ this.headers.clear();
+ } else {
+ this.headers.putAll(headers);
+ }
+ return this;
+ }
+
+ /**
+ * Returns an immutable copy of the current headers.
+ *
+ * @see Request#headers()
+ */
+ public Map> headers() {
+ return Collections.unmodifiableMap(headers);
+ }
+
+ /**
+ * replaces the {@link feign.Util#CONTENT_LENGTH} header. Usually populated by an {@link
+ * feign.codec.Encoder}.
+ *
+ * @see Request#body()
+ */
+ public RequestTemplate body(byte[] bodyData, Charset charset) {
+ this.bodyTemplate = null;
+ this.charset = charset;
+ this.body = bodyData;
+ int bodyLength = bodyData != null ? bodyData.length : 0;
+ header(CONTENT_LENGTH, String.valueOf(bodyLength));
+ return this;
+ }
+
+ /**
+ * replaces the {@link feign.Util#CONTENT_LENGTH} header. Usually populated by an {@link
+ * feign.codec.Encoder}.
+ *
+ * @see Request#body()
+ */
+ public RequestTemplate body(String bodyText) {
+ byte[] bodyData = bodyText != null ? bodyText.getBytes(UTF_8) : null;
+ return body(bodyData, UTF_8);
+ }
+
+ /**
+ * The character set with which the body is encoded, or null if unknown or not applicable. When
+ * this is present, you can use {@code new String(req.body(), req.charset())} to access the body
+ * as a String.
+ */
+ public Charset charset() {
+ return charset;
+ }
+
+ /**
+ * @see Request#body()
+ */
+ public byte[] body() {
+ return body;
+ }
+
+ /**
+ * populated by {@link Body}
+ *
+ * @see Request#body()
+ */
+ public RequestTemplate bodyTemplate(String bodyTemplate) {
+ this.bodyTemplate = bodyTemplate;
+ this.charset = null;
+ this.body = null;
+ return this;
+ }
+
+ /**
+ * @see Request#body()
+ * @see #expand(String, Map)
+ */
+ public String bodyTemplate() {
+ return bodyTemplate;
+ }
+
+ /**
+ * if there are any query params in the URL, this will extract them out.
+ */
+ private StringBuilder pullAnyQueriesOutOfUrl(StringBuilder url) {
+ // parse out queries
+ int queryIndex = url.indexOf("?");
+ if (queryIndex != -1) {
+ String queryLine = url.substring(queryIndex + 1);
+ Map> firstQueries = parseAndDecodeQueries(queryLine);
+ if (!queries.isEmpty()) {
+ firstQueries.putAll(queries);
+ queries.clear();
+ }
+ //Since we decode all queries, we want to use the
+ //query()-method to re-add them to ensure that all
+ //logic (such as url-encoding) are executed, giving
+ //a valid queryLine()
+ for (String key : firstQueries.keySet()) {
+ Collection values = firstQueries.get(key);
+ if (allValuesAreNull(values)) {
+ //Queries where all values are null will
+ //be ignored by the query(key, value)-method
+ //So we manually avoid this case here, to ensure that
+ //we still fulfill the contract (ex. parameters without values)
+ queries.put(urlEncode(key), values);
+ } else {
+ query(key, values);
+ }
+
+ }
+ return new StringBuilder(url.substring(0, queryIndex));
+ }
+ return url;
+ }
+
+ private boolean allValuesAreNull(Collection values) {
+ if (values == null || values.isEmpty()) {
+ return true;
+ }
+ for (String val : values) {
+ if (val != null) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return request().toString();
+ }
+
+ /** {@link #replaceQueryValues(Map, Map)}, which assumes no parameter is encoded */
+ public void replaceQueryValues(Map unencoded) {
+ replaceQueryValues(unencoded, Collections.emptyMap());
+ }
+
+ /**
+ * Replaces query values which are templated with corresponding values from the {@code unencoded}
+ * map. Any unresolved queries are removed.
+ */
+ void replaceQueryValues(Map unencoded, Map alreadyEncoded) {
+ Iterator>> iterator = queries.entrySet().iterator();
+ while (iterator.hasNext()) {
+ Entry> entry = iterator.next();
+ if (entry.getValue() == null) {
+ continue;
+ }
+ Collection values = new ArrayList();
+ for (String value : entry.getValue()) {
+ if (value.indexOf('{') == 0 && value.indexOf('}') == value.length() - 1) {
+ Object variableValue = unencoded.get(value.substring(1, value.length() - 1));
+ // only add non-null expressions
+ if (variableValue == null) {
+ continue;
+ }
+ if (variableValue instanceof Iterable) {
+ for (Object val : Iterable.class.cast(variableValue)) {
+ String encodedValue = encodeValueIfNotEncoded(entry.getKey(), val, alreadyEncoded);
+ values.add(encodedValue);
+ }
+ } else {
+ String encodedValue = encodeValueIfNotEncoded(entry.getKey(), variableValue, alreadyEncoded);
+ values.add(encodedValue);
+ }
+ } else {
+ values.add(value);
+ }
+ }
+ if (values.isEmpty()) {
+ iterator.remove();
+ } else {
+ entry.setValue(values);
+ }
+ }
+ }
+
+ public String queryLine() {
+ if (queries.isEmpty()) {
+ return "";
+ }
+ StringBuilder queryBuilder = new StringBuilder();
+ for (String field : queries.keySet()) {
+ for (String value : valuesOrEmpty(queries, field)) {
+ queryBuilder.append('&');
+ queryBuilder.append(field);
+ if (value != null) {
+ queryBuilder.append('=');
+ if (!value.isEmpty()) {
+ queryBuilder.append(value);
+ }
+ }
+ }
+ }
+ queryBuilder.deleteCharAt(0);
+ return queryBuilder.insert(0, '?').toString();
+ }
+
+ interface Factory {
+
+ /**
+ * create a request template using args passed to a method invocation.
+ */
+ RequestTemplate create(Object[] argv);
+ }
+}
diff --git a/core/src/main/java/feign/Response.java b/core/src/main/java/feign/Response.java
new file mode 100644
index 0000000000..e9f03fda5d
--- /dev/null
+++ b/core/src/main/java/feign/Response.java
@@ -0,0 +1,388 @@
+/*
+ * Copyright 2013 Netflix, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package feign;
+
+import java.io.ByteArrayInputStream;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.Charset;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+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.
+ */
+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);
+ this.status = builder.status;
+ this.reason = builder.reason; //nullable
+ this.headers = Collections.unmodifiableMap(caseInsensitiveCopyOf(builder.headers));
+ this.body = builder.body; //nullable
+ this.request = builder.request; //nullable
+ }
+
+ /**
+ * @deprecated To be removed in Feign 10
+ */
+ @Deprecated
+ public static Response create(int status, String reason, Map> headers,
+ InputStream inputStream, Integer length) {
+ return Response.builder()
+ .status(status)
+ .reason(reason)
+ .headers(headers)
+ .body(InputStreamBody.orNull(inputStream, length))
+ .build();
+ }
+
+ /**
+ * @deprecated To be removed in Feign 10
+ */
+ @Deprecated
+ public static Response create(int status, String reason, Map> headers,
+ byte[] data) {
+ return Response.builder()
+ .status(status)
+ .reason(reason)
+ .headers(headers)
+ .body(ByteArrayBody.orNull(data))
+ .build();
+ }
+
+ /**
+ * @deprecated To be removed in Feign 10
+ */
+ @Deprecated
+ public static Response create(int status, String reason, Map> headers,
+ String text, Charset charset) {
+ return Response.builder()
+ .status(status)
+ .reason(reason)
+ .headers(headers)
+ .body(ByteArrayBody.orNull(text, charset))
+ .build();
+ }
+
+ /**
+ * @deprecated To be removed in Feign 10
+ */
+ @Deprecated
+ public static Response create(int status, String reason, Map> headers,
+ Body body) {
+ return Response.builder()
+ .status(status)
+ .reason(reason)
+ .headers(headers)
+ .body(body)
+ .build();
+ }
+
+ 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;
+ }
+
+ /** @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
+ *
+ * NOTE: will add null check in version 10 which may require changes
+ * to custom feign.Client or loggers
+ */
+ public Builder request(Request request) {
+ 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;
+ }
+
+ /**
+ * if present, 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;
+ }
+
+ 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 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 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/main/java/feign/ResponseMapper.java b/core/src/main/java/feign/ResponseMapper.java
new file mode 100644
index 0000000000..92d1999072
--- /dev/null
+++ b/core/src/main/java/feign/ResponseMapper.java
@@ -0,0 +1,27 @@
+package feign;
+
+import java.lang.reflect.Type;
+
+/**
+ * Map function to apply to the response before decoding it.
+ *
+ * {@code
+ * new ResponseMapper() {
+ * @Override
+ * public Response map(Response response, Type type) {
+ * try {
+ * return response
+ * .toBuilder()
+ * .body(Util.toString(response.body().asReader()).toUpperCase().getBytes())
+ * .build();
+ * } catch (IOException e) {
+ * throw new RuntimeException(e);
+ * }
+ * }
+ * };
+ * }
+ */
+public interface ResponseMapper {
+
+ Response map(Response response, Type type);
+}
diff --git a/core/src/main/java/feign/RetryableException.java b/core/src/main/java/feign/RetryableException.java
new file mode 100644
index 0000000000..ff91ba0db4
--- /dev/null
+++ b/core/src/main/java/feign/RetryableException.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2013 Netflix, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package feign;
+
+import java.util.Date;
+
+/**
+ * This exception is raised when the {@link Response} is deemed to be retryable, typically via an
+ * {@link feign.codec.ErrorDecoder} when the {@link Response#status() status} is 503.
+ */
+public class RetryableException extends FeignException {
+
+ private static final long serialVersionUID = 1L;
+
+ private final Long retryAfter;
+
+ /**
+ * @param retryAfter usually corresponds to the {@link feign.Util#RETRY_AFTER} header.
+ */
+ public RetryableException(String message, Throwable cause, Date retryAfter) {
+ super(message, cause);
+ this.retryAfter = retryAfter != null ? retryAfter.getTime() : null;
+ }
+
+ /**
+ * @param retryAfter usually corresponds to the {@link feign.Util#RETRY_AFTER} header.
+ */
+ public RetryableException(String message, Date retryAfter) {
+ super(message);
+ this.retryAfter = retryAfter != null ? retryAfter.getTime() : null;
+ }
+
+ /**
+ * Sometimes corresponds to the {@link feign.Util#RETRY_AFTER} header present in {@code 503}
+ * status. Other times parsed from an application-specific response. Null if unknown.
+ */
+ public Date retryAfter() {
+ return retryAfter != null ? new Date(retryAfter) : null;
+ }
+}
diff --git a/core/src/main/java/feign/Retryer.java b/core/src/main/java/feign/Retryer.java
new file mode 100644
index 0000000000..8a29d34cf0
--- /dev/null
+++ b/core/src/main/java/feign/Retryer.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2013 Netflix, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package feign;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+/**
+ * Cloned for each invocation to {@link Client#execute(Request, feign.Request.Options)}.
+ * Implementations may keep state to determine if retry operations should continue or not.
+ */
+public interface Retryer extends Cloneable {
+
+ /**
+ * if retry is permitted, return (possibly after sleeping). Otherwise propagate the exception.
+ */
+ void continueOrPropagate(RetryableException e);
+
+ Retryer clone();
+
+ public static class Default implements Retryer {
+
+ private final int maxAttempts;
+ private final long period;
+ private final long maxPeriod;
+ int attempt;
+ long sleptForMillis;
+
+ public Default() {
+ this(100, SECONDS.toMillis(1), 5);
+ }
+
+ public Default(long period, long maxPeriod, int maxAttempts) {
+ this.period = period;
+ this.maxPeriod = maxPeriod;
+ this.maxAttempts = maxAttempts;
+ this.attempt = 1;
+ }
+
+ // visible for testing;
+ protected long currentTimeMillis() {
+ return System.currentTimeMillis();
+ }
+
+ public void continueOrPropagate(RetryableException e) {
+ if (attempt++ >= maxAttempts) {
+ throw e;
+ }
+
+ long interval;
+ if (e.retryAfter() != null) {
+ interval = e.retryAfter().getTime() - currentTimeMillis();
+ if (interval > maxPeriod) {
+ interval = maxPeriod;
+ }
+ if (interval < 0) {
+ return;
+ }
+ } else {
+ interval = nextMaxInterval();
+ }
+ try {
+ Thread.sleep(interval);
+ } catch (InterruptedException ignored) {
+ Thread.currentThread().interrupt();
+ }
+ sleptForMillis += interval;
+ }
+
+ /**
+ * Calculates the time interval to a retry attempt. The interval increases exponentially
+ * with each attempt, at a rate of nextInterval *= 1.5 (where 1.5 is the backoff factor), to the
+ * maximum interval.
+ *
+ * @return time in nanoseconds from now until the next attempt.
+ */
+ long nextMaxInterval() {
+ long interval = (long) (period * Math.pow(1.5, attempt - 1));
+ return interval > maxPeriod ? maxPeriod : interval;
+ }
+
+ @Override
+ public Retryer clone() {
+ return new Default(period, maxPeriod, maxAttempts);
+ }
+ }
+
+ /**
+ * Implementation that never retries request. It propagates the RetryableException.
+ */
+ Retryer NEVER_RETRY = new Retryer() {
+
+ @Override
+ public void continueOrPropagate(RetryableException e) {
+ throw e;
+ }
+
+ @Override
+ public Retryer clone() {
+ return this;
+ }
+ };
+}
diff --git a/core/src/main/java/feign/SynchronousMethodHandler.java b/core/src/main/java/feign/SynchronousMethodHandler.java
new file mode 100644
index 0000000000..c6c360e0ca
--- /dev/null
+++ b/core/src/main/java/feign/SynchronousMethodHandler.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2014 Netflix, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package feign;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import feign.InvocationHandlerFactory.MethodHandler;
+import feign.Request.Options;
+import feign.codec.DecodeException;
+import feign.codec.Decoder;
+import feign.codec.ErrorDecoder;
+
+import static feign.FeignException.errorExecuting;
+import static feign.FeignException.errorReading;
+import static feign.Util.checkNotNull;
+import static feign.Util.ensureClosed;
+
+final class SynchronousMethodHandler implements MethodHandler {
+
+ private static final long MAX_RESPONSE_BUFFER_SIZE = 8192L;
+
+ private final MethodMetadata metadata;
+ private final Target> target;
+ private final Client client;
+ private final Retryer retryer;
+ private final List requestInterceptors;
+ private final Logger logger;
+ private final Logger.Level logLevel;
+ private final RequestTemplate.Factory buildTemplateFromArgs;
+ private final Options options;
+ private final Decoder decoder;
+ private final ErrorDecoder errorDecoder;
+ private final boolean decode404;
+
+ private SynchronousMethodHandler(Target> target, Client client, Retryer retryer,
+ List requestInterceptors, Logger logger,
+ Logger.Level logLevel, MethodMetadata metadata,
+ RequestTemplate.Factory buildTemplateFromArgs, Options options,
+ Decoder decoder, ErrorDecoder errorDecoder, boolean decode404) {
+ this.target = checkNotNull(target, "target");
+ this.client = checkNotNull(client, "client for %s", target);
+ this.retryer = checkNotNull(retryer, "retryer for %s", target);
+ this.requestInterceptors =
+ checkNotNull(requestInterceptors, "requestInterceptors for %s", target);
+ this.logger = checkNotNull(logger, "logger for %s", target);
+ this.logLevel = checkNotNull(logLevel, "logLevel for %s", target);
+ this.metadata = checkNotNull(metadata, "metadata for %s", target);
+ this.buildTemplateFromArgs = checkNotNull(buildTemplateFromArgs, "metadata for %s", target);
+ this.options = checkNotNull(options, "options for %s", target);
+ this.errorDecoder = checkNotNull(errorDecoder, "errorDecoder for %s", target);
+ this.decoder = checkNotNull(decoder, "decoder for %s", target);
+ this.decode404 = decode404;
+ }
+
+ @Override
+ public Object invoke(Object[] argv) throws Throwable {
+ RequestTemplate template = buildTemplateFromArgs.create(argv);
+ Retryer retryer = this.retryer.clone();
+ while (true) {
+ try {
+ return executeAndDecode(template);
+ } catch (RetryableException e) {
+ retryer.continueOrPropagate(e);
+ if (logLevel != Logger.Level.NONE) {
+ logger.logRetry(metadata.configKey(), logLevel);
+ }
+ continue;
+ }
+ }
+ }
+
+ Object executeAndDecode(RequestTemplate template) throws Throwable {
+ Request request = targetRequest(template);
+
+ if (logLevel != Logger.Level.NONE) {
+ logger.logRequest(metadata.configKey(), logLevel, request);
+ }
+
+ Response response;
+ long start = System.nanoTime();
+ try {
+ response = client.execute(request, options);
+ // ensure the request is set. TODO: remove in Feign 10
+ response.toBuilder().request(request).build();
+ } catch (IOException e) {
+ if (logLevel != Logger.Level.NONE) {
+ logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
+ }
+ throw errorExecuting(request, e);
+ }
+ long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
+
+ boolean shouldClose = true;
+ try {
+ if (logLevel != Logger.Level.NONE) {
+ response =
+ logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime);
+ // ensure the request is set. TODO: remove in Feign 10
+ response.toBuilder().request(request).build();
+ }
+ if (Response.class == metadata.returnType()) {
+ if (response.body() == null) {
+ return response;
+ }
+ if (response.body().length() == null ||
+ response.body().length() > MAX_RESPONSE_BUFFER_SIZE) {
+ shouldClose = false;
+ return response;
+ }
+ // Ensure the response body is disconnected
+ byte[] bodyData = Util.toByteArray(response.body().asInputStream());
+ return response.toBuilder().body(bodyData).build();
+ }
+ if (response.status() >= 200 && response.status() < 300) {
+ if (void.class == metadata.returnType()) {
+ return null;
+ } else {
+ return decode(response);
+ }
+ } else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) {
+ return decode(response);
+ } else {
+ throw errorDecoder.decode(metadata.configKey(), response);
+ }
+ } catch (IOException e) {
+ if (logLevel != Logger.Level.NONE) {
+ logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime);
+ }
+ throw errorReading(request, response, e);
+ } finally {
+ if (shouldClose) {
+ ensureClosed(response.body());
+ }
+ }
+ }
+
+ long elapsedTime(long start) {
+ return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
+ }
+
+ Request targetRequest(RequestTemplate template) {
+ for (RequestInterceptor interceptor : requestInterceptors) {
+ interceptor.apply(template);
+ }
+ return target.apply(new RequestTemplate(template));
+ }
+
+ Object decode(Response response) throws Throwable {
+ try {
+ return decoder.decode(response, metadata.returnType());
+ } catch (FeignException e) {
+ throw e;
+ } catch (RuntimeException e) {
+ throw new DecodeException(e.getMessage(), e);
+ }
+ }
+
+ static class Factory {
+
+ private final Client client;
+ private final Retryer retryer;
+ private final List requestInterceptors;
+ private final Logger logger;
+ private final Logger.Level logLevel;
+ private final boolean decode404;
+
+ Factory(Client client, Retryer retryer, List requestInterceptors,
+ Logger logger, Logger.Level logLevel, boolean decode404) {
+ this.client = checkNotNull(client, "client");
+ this.retryer = checkNotNull(retryer, "retryer");
+ this.requestInterceptors = checkNotNull(requestInterceptors, "requestInterceptors");
+ this.logger = checkNotNull(logger, "logger");
+ this.logLevel = checkNotNull(logLevel, "logLevel");
+ this.decode404 = decode404;
+ }
+
+ public MethodHandler create(Target> target, MethodMetadata md,
+ RequestTemplate.Factory buildTemplateFromArgs,
+ Options options, Decoder decoder, ErrorDecoder errorDecoder) {
+ return new SynchronousMethodHandler(target, client, retryer, requestInterceptors, logger,
+ logLevel, md, buildTemplateFromArgs, options, decoder,
+ errorDecoder, decode404);
+ }
+ }
+}
diff --git a/core/src/main/java/feign/Target.java b/core/src/main/java/feign/Target.java
new file mode 100644
index 0000000000..2c82067fbd
--- /dev/null
+++ b/core/src/main/java/feign/Target.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright 2013 Netflix, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package feign;
+
+import static feign.Util.checkNotNull;
+import static feign.Util.emptyToNull;
+
+/**
+ * relationship to JAXRS 2.0 Similar to {@code
+ * javax.ws.rs.client.WebTarget}, as it produces requests. However, {@link RequestTemplate} is a
+ * closer match to {@code WebTarget}.
+ *
+ * @param type of the interface this target applies to.
+ */
+public interface Target {
+
+ /* The type of the interface this target applies to. ex. {@code Route53}. */
+ Class type();
+
+ /* configuration key associated with this target. For example, {@code route53}. */
+ String name();
+
+ /* base HTTP URL of the target. For example, {@code https://api/v2}. */
+ String url();
+
+ /**
+ * Targets a template to this target, adding the {@link #url() base url} and any target-specific
+ * headers or query parameters. For example:
+ *
+ * public Request apply(RequestTemplate input) {
+ * input.insert(0, url());
+ * input.replaceHeader("X-Auth", currentToken);
+ * return input.asRequest();
+ * }
+ *
+ * relationship to JAXRS 2.0 This call is similar to {@code
+ * javax.ws.rs.client.WebTarget.request()}, except that we expect transient, but necessary
+ * decoration to be applied on invocation.
+ */
+ public Request apply(RequestTemplate input);
+
+ public static class HardCodedTarget implements Target {
+
+ private final Class type;
+ private final String name;
+ private final String url;
+
+ public HardCodedTarget(Class type, String url) {
+ this(type, url, url);
+ }
+
+ public HardCodedTarget(Class type, String name, String url) {
+ this.type = checkNotNull(type, "type");
+ this.name = checkNotNull(emptyToNull(name), "name");
+ this.url = checkNotNull(emptyToNull(url), "url");
+ }
+
+ @Override
+ public Class type() {
+ return type;
+ }
+
+ @Override
+ public String name() {
+ return name;
+ }
+
+ @Override
+ public String url() {
+ return url;
+ }
+
+ /* no authentication or other special activity. just insert the url. */
+ @Override
+ public Request apply(RequestTemplate input) {
+ if (input.url().indexOf("http") != 0) {
+ input.insert(0, url());
+ }
+ return input.request();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof HardCodedTarget) {
+ HardCodedTarget> other = (HardCodedTarget) obj;
+ return type.equals(other.type)
+ && name.equals(other.name)
+ && url.equals(other.url);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + type.hashCode();
+ result = 31 * result + name.hashCode();
+ result = 31 * result + url.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ if (name.equals(url)) {
+ return "HardCodedTarget(type=" + type.getSimpleName() + ", url=" + url + ")";
+ }
+ return "HardCodedTarget(type=" + type.getSimpleName() + ", name=" + name + ", url=" + url
+ + ")";
+ }
+ }
+
+ public static final class EmptyTarget implements Target {
+
+ private final Class type;
+ private final String name;
+
+ EmptyTarget(Class type, String name) {
+ this.type = checkNotNull(type, "type");
+ this.name = checkNotNull(emptyToNull(name), "name");
+ }
+
+ public static EmptyTarget create(Class type) {
+ return new EmptyTarget(type, "empty:" + type.getSimpleName());
+ }
+
+ public static EmptyTarget create(Class type, String name) {
+ return new EmptyTarget(type, name);
+ }
+
+ @Override
+ public Class type() {
+ return type;
+ }
+
+ @Override
+ public String name() {
+ return name;
+ }
+
+ @Override
+ public String url() {
+ throw new UnsupportedOperationException("Empty targets don't have URLs");
+ }
+
+ @Override
+ public Request apply(RequestTemplate input) {
+ if (input.url().indexOf("http") != 0) {
+ throw new UnsupportedOperationException(
+ "Request with non-absolute URL not supported with empty target");
+ }
+ return input.request();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof EmptyTarget) {
+ EmptyTarget> other = (EmptyTarget) obj;
+ return type.equals(other.type)
+ && name.equals(other.name);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + type.hashCode();
+ result = 31 * result + name.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ if (name.equals("empty:" + type.getSimpleName())) {
+ return "EmptyTarget(type=" + type.getSimpleName() + ")";
+ }
+ return "EmptyTarget(type=" + type.getSimpleName() + ", name=" + name + ")";
+ }
+ }
+}
diff --git a/core/src/main/java/feign/Types.java b/core/src/main/java/feign/Types.java
new file mode 100644
index 0000000000..2b8e74f0a5
--- /dev/null
+++ b/core/src/main/java/feign/Types.java
@@ -0,0 +1,467 @@
+/*
+ * Copyright (C) 2008 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package feign;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.GenericArrayType;
+import java.lang.reflect.GenericDeclaration;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.lang.reflect.WildcardType;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.NoSuchElementException;
+
+/**
+ * Static methods for working with types.
+ *
+ * @author Bob Lee
+ * @author Jesse Wilson
+ */
+final class Types {
+
+ private static final Type[] EMPTY_TYPE_ARRAY = new Type[0];
+
+ private Types() {
+ // No instances.
+ }
+
+ static Class> getRawType(Type type) {
+ if (type instanceof Class>) {
+ // Type is a normal class.
+ return (Class>) type;
+
+ } else if (type instanceof ParameterizedType) {
+ ParameterizedType parameterizedType = (ParameterizedType) type;
+
+ // I'm not exactly sure why getRawType() returns Type instead of Class. Neal isn't either but
+ // suspects some pathological case related to nested classes exists.
+ Type rawType = parameterizedType.getRawType();
+ if (!(rawType instanceof Class)) {
+ throw new IllegalArgumentException();
+ }
+ return (Class>) rawType;
+
+ } else if (type instanceof GenericArrayType) {
+ Type componentType = ((GenericArrayType) type).getGenericComponentType();
+ return Array.newInstance(getRawType(componentType), 0).getClass();
+
+ } else if (type instanceof TypeVariable) {
+ // We could use the variable's bounds, but that won't work if there are multiple. Having a raw
+ // type that's more general than necessary is okay.
+ return Object.class;
+
+ } else if (type instanceof WildcardType) {
+ return getRawType(((WildcardType) type).getUpperBounds()[0]);
+
+ } else {
+ String className = type == null ? "null" : type.getClass().getName();
+ throw new IllegalArgumentException("Expected a Class, ParameterizedType, or "
+ + "GenericArrayType, but <" + type + "> is of type "
+ + className);
+ }
+ }
+
+ /**
+ * Returns true if {@code a} and {@code b} are equal.
+ */
+ static boolean equals(Type a, Type b) {
+ if (a == b) {
+ return true; // Also handles (a == null && b == null).
+
+ } else if (a instanceof Class) {
+ return a.equals(b); // Class already specifies equals().
+
+ } else if (a instanceof ParameterizedType) {
+ if (!(b instanceof ParameterizedType)) {
+ return false;
+ }
+ ParameterizedType pa = (ParameterizedType) a;
+ ParameterizedType pb = (ParameterizedType) b;
+ return equal(pa.getOwnerType(), pb.getOwnerType())
+ && pa.getRawType().equals(pb.getRawType())
+ && Arrays.equals(pa.getActualTypeArguments(), pb.getActualTypeArguments());
+
+ } else if (a instanceof GenericArrayType) {
+ if (!(b instanceof GenericArrayType)) {
+ return false;
+ }
+ GenericArrayType ga = (GenericArrayType) a;
+ GenericArrayType gb = (GenericArrayType) b;
+ return equals(ga.getGenericComponentType(), gb.getGenericComponentType());
+
+ } else if (a instanceof WildcardType) {
+ if (!(b instanceof WildcardType)) {
+ return false;
+ }
+ WildcardType wa = (WildcardType) a;
+ WildcardType wb = (WildcardType) b;
+ return Arrays.equals(wa.getUpperBounds(), wb.getUpperBounds())
+ && Arrays.equals(wa.getLowerBounds(), wb.getLowerBounds());
+
+ } else if (a instanceof TypeVariable) {
+ if (!(b instanceof TypeVariable)) {
+ return false;
+ }
+ TypeVariable> va = (TypeVariable>) a;
+ TypeVariable> vb = (TypeVariable>) b;
+ return va.getGenericDeclaration() == vb.getGenericDeclaration()
+ && va.getName().equals(vb.getName());
+
+ } else {
+ return false; // This isn't a type we support!
+ }
+ }
+
+ /**
+ * Returns the generic supertype for {@code supertype}. For example, given a class {@code
+ * IntegerSet}, the result for when supertype is {@code Set.class} is {@code Set} and the
+ * result when the supertype is {@code Collection.class} is {@code Collection}.
+ */
+ static Type getGenericSupertype(Type context, Class> rawType, Class> toResolve) {
+ if (toResolve == rawType) {
+ return context;
+ }
+
+ // We skip searching through interfaces if unknown is an interface.
+ if (toResolve.isInterface()) {
+ Class>[] interfaces = rawType.getInterfaces();
+ for (int i = 0, length = interfaces.length; i < length; i++) {
+ if (interfaces[i] == toResolve) {
+ return rawType.getGenericInterfaces()[i];
+ } else if (toResolve.isAssignableFrom(interfaces[i])) {
+ return getGenericSupertype(rawType.getGenericInterfaces()[i], interfaces[i], toResolve);
+ }
+ }
+ }
+
+ // Check our supertypes.
+ if (!rawType.isInterface()) {
+ while (rawType != Object.class) {
+ Class> rawSupertype = rawType.getSuperclass();
+ if (rawSupertype == toResolve) {
+ return rawType.getGenericSuperclass();
+ } else if (toResolve.isAssignableFrom(rawSupertype)) {
+ return getGenericSupertype(rawType.getGenericSuperclass(), rawSupertype, toResolve);
+ }
+ rawType = rawSupertype;
+ }
+ }
+
+ // We can't resolve this further.
+ return toResolve;
+ }
+
+ private static int indexOf(Object[] array, Object toFind) {
+ for (int i = 0; i < array.length; i++) {
+ if (toFind.equals(array[i])) {
+ return i;
+ }
+ }
+ throw new NoSuchElementException();
+ }
+
+ private static boolean equal(Object a, Object b) {
+ return a == b || (a != null && a.equals(b));
+ }
+
+ private static int hashCodeOrZero(Object o) {
+ return o != null ? o.hashCode() : 0;
+ }
+
+ static String typeToString(Type type) {
+ return type instanceof Class ? ((Class>) type).getName() : type.toString();
+ }
+
+ /**
+ * Returns the generic form of {@code supertype}. For example, if this is {@code
+ * ArrayList}, this returns {@code Iterable} given the input {@code
+ * Iterable.class}.
+ *
+ * @param supertype a superclass of, or interface implemented by, this.
+ */
+ static Type getSupertype(Type context, Class> contextRawType, Class> supertype) {
+ if (!supertype.isAssignableFrom(contextRawType)) {
+ throw new IllegalArgumentException();
+ }
+ return resolve(context, contextRawType,
+ getGenericSupertype(context, contextRawType, supertype));
+ }
+
+ static Type resolve(Type context, Class> contextRawType, Type toResolve) {
+ // This implementation is made a little more complicated in an attempt to avoid object-creation.
+ while (true) {
+ if (toResolve instanceof TypeVariable) {
+ TypeVariable> typeVariable = (TypeVariable>) toResolve;
+ toResolve = resolveTypeVariable(context, contextRawType, typeVariable);
+ if (toResolve == typeVariable) {
+ return toResolve;
+ }
+
+ } else if (toResolve instanceof Class && ((Class>) toResolve).isArray()) {
+ Class> original = (Class>) toResolve;
+ Type componentType = original.getComponentType();
+ Type newComponentType = resolve(context, contextRawType, componentType);
+ return componentType == newComponentType ? original : new GenericArrayTypeImpl(
+ newComponentType);
+
+ } else if (toResolve instanceof GenericArrayType) {
+ GenericArrayType original = (GenericArrayType) toResolve;
+ Type componentType = original.getGenericComponentType();
+ Type newComponentType = resolve(context, contextRawType, componentType);
+ return componentType == newComponentType ? original : new GenericArrayTypeImpl(
+ newComponentType);
+
+ } else if (toResolve instanceof ParameterizedType) {
+ ParameterizedType original = (ParameterizedType) toResolve;
+ Type ownerType = original.getOwnerType();
+ Type newOwnerType = resolve(context, contextRawType, ownerType);
+ boolean changed = newOwnerType != ownerType;
+
+ Type[] args = original.getActualTypeArguments();
+ for (int t = 0, length = args.length; t < length; t++) {
+ Type resolvedTypeArgument = resolve(context, contextRawType, args[t]);
+ if (resolvedTypeArgument != args[t]) {
+ if (!changed) {
+ args = args.clone();
+ changed = true;
+ }
+ args[t] = resolvedTypeArgument;
+ }
+ }
+
+ return changed
+ ? new ParameterizedTypeImpl(newOwnerType, original.getRawType(), args)
+ : original;
+
+ } else if (toResolve instanceof WildcardType) {
+ WildcardType original = (WildcardType) toResolve;
+ Type[] originalLowerBound = original.getLowerBounds();
+ Type[] originalUpperBound = original.getUpperBounds();
+
+ if (originalLowerBound.length == 1) {
+ Type lowerBound = resolve(context, contextRawType, originalLowerBound[0]);
+ if (lowerBound != originalLowerBound[0]) {
+ return new WildcardTypeImpl(new Type[]{Object.class}, new Type[]{lowerBound});
+ }
+ } else if (originalUpperBound.length == 1) {
+ Type upperBound = resolve(context, contextRawType, originalUpperBound[0]);
+ if (upperBound != originalUpperBound[0]) {
+ return new WildcardTypeImpl(new Type[]{upperBound}, EMPTY_TYPE_ARRAY);
+ }
+ }
+ return original;
+
+ } else {
+ return toResolve;
+ }
+ }
+ }
+
+ private static Type resolveTypeVariable(
+ Type context, Class> contextRawType, TypeVariable> unknown) {
+ Class> declaredByRaw = declaringClassOf(unknown);
+
+ // We can't reduce this further.
+ if (declaredByRaw == null) {
+ return unknown;
+ }
+
+ Type declaredBy = getGenericSupertype(context, contextRawType, declaredByRaw);
+ if (declaredBy instanceof ParameterizedType) {
+ int index = indexOf(declaredByRaw.getTypeParameters(), unknown);
+ return ((ParameterizedType) declaredBy).getActualTypeArguments()[index];
+ }
+
+ return unknown;
+ }
+
+ /**
+ * Returns the declaring class of {@code typeVariable}, or {@code null} if it was not declared by
+ * a class.
+ */
+ private static Class> declaringClassOf(TypeVariable> typeVariable) {
+ GenericDeclaration genericDeclaration = typeVariable.getGenericDeclaration();
+ return genericDeclaration instanceof Class ? (Class>) genericDeclaration : null;
+ }
+
+ private static void checkNotPrimitive(Type type) {
+ if (type instanceof Class> && ((Class>) type).isPrimitive()) {
+ throw new IllegalArgumentException();
+ }
+ }
+
+ static final class ParameterizedTypeImpl implements ParameterizedType {
+
+ private final Type ownerType;
+ private final Type rawType;
+ private final Type[] typeArguments;
+
+ ParameterizedTypeImpl(Type ownerType, Type rawType, Type... typeArguments) {
+ // Require an owner type if the raw type needs it.
+ if (rawType instanceof Class>
+ && (ownerType == null) != (((Class>) rawType).getEnclosingClass() == null)) {
+ throw new IllegalArgumentException();
+ }
+
+ this.ownerType = ownerType;
+ this.rawType = rawType;
+ this.typeArguments = typeArguments.clone();
+
+ for (Type typeArgument : this.typeArguments) {
+ if (typeArgument == null) {
+ throw new NullPointerException();
+ }
+ checkNotPrimitive(typeArgument);
+ }
+ }
+
+ public Type[] getActualTypeArguments() {
+ return typeArguments.clone();
+ }
+
+ public Type getRawType() {
+ return rawType;
+ }
+
+ public Type getOwnerType() {
+ return ownerType;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other instanceof ParameterizedType && Types.equals(this, (ParameterizedType) other);
+ }
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(typeArguments) ^ rawType.hashCode() ^ hashCodeOrZero(ownerType);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder(30 * (typeArguments.length + 1));
+ result.append(typeToString(rawType));
+ if (typeArguments.length == 0) {
+ return result.toString();
+ }
+ result.append("<").append(typeToString(typeArguments[0]));
+ for (int i = 1; i < typeArguments.length; i++) {
+ result.append(", ").append(typeToString(typeArguments[i]));
+ }
+ return result.append(">").toString();
+ }
+ }
+
+ private static final class GenericArrayTypeImpl implements GenericArrayType {
+
+ private final Type componentType;
+
+ GenericArrayTypeImpl(Type componentType) {
+ this.componentType = componentType;
+ }
+
+ public Type getGenericComponentType() {
+ return componentType;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return o instanceof GenericArrayType
+ && Types.equals(this, (GenericArrayType) o);
+ }
+
+ @Override
+ public int hashCode() {
+ return componentType.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return typeToString(componentType) + "[]";
+ }
+ }
+
+ /**
+ * The WildcardType interface supports multiple upper bounds and multiple lower bounds. We only
+ * support what the Java 6 language needs - at most one bound. If a lower bound is set, the upper
+ * bound must be Object.class.
+ */
+ static final class WildcardTypeImpl implements WildcardType {
+
+ private final Type upperBound;
+ private final Type lowerBound;
+
+ WildcardTypeImpl(Type[] upperBounds, Type[] lowerBounds) {
+ if (lowerBounds.length > 1) {
+ throw new IllegalArgumentException();
+ }
+ if (upperBounds.length != 1) {
+ throw new IllegalArgumentException();
+ }
+
+ if (lowerBounds.length == 1) {
+ if (lowerBounds[0] == null) {
+ throw new NullPointerException();
+ }
+ checkNotPrimitive(lowerBounds[0]);
+ if (upperBounds[0] != Object.class) {
+ throw new IllegalArgumentException();
+ }
+ this.lowerBound = lowerBounds[0];
+ this.upperBound = Object.class;
+ } else {
+ if (upperBounds[0] == null) {
+ throw new NullPointerException();
+ }
+ checkNotPrimitive(upperBounds[0]);
+ this.lowerBound = null;
+ this.upperBound = upperBounds[0];
+ }
+ }
+
+ public Type[] getUpperBounds() {
+ return new Type[]{upperBound};
+ }
+
+ public Type[] getLowerBounds() {
+ return lowerBound != null ? new Type[]{lowerBound} : EMPTY_TYPE_ARRAY;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other instanceof WildcardType && Types.equals(this, (WildcardType) other);
+ }
+
+ @Override
+ public int hashCode() {
+ // This equals Arrays.hashCode(getLowerBounds()) ^ Arrays.hashCode(getUpperBounds()).
+ return (lowerBound != null ? 31 + lowerBound.hashCode() : 1) ^ (31 + upperBound.hashCode());
+ }
+
+ @Override
+ public String toString() {
+ if (lowerBound != null) {
+ return "? super " + typeToString(lowerBound);
+ }
+ if (upperBound == Object.class) {
+ return "?";
+ }
+ return "? extends " + typeToString(upperBound);
+ }
+ }
+}
diff --git a/core/src/main/java/feign/Util.java b/core/src/main/java/feign/Util.java
new file mode 100644
index 0000000000..46f6ec9bb6
--- /dev/null
+++ b/core/src/main/java/feign/Util.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright 2013 Netflix, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package feign;
+
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.lang.reflect.Array;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.WildcardType;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+import static java.lang.String.format;
+
+/**
+ * Utilities, typically copied in from guava, so as to avoid dependency conflicts.
+ */
+public class Util {
+
+ /**
+ * The HTTP Content-Length header field name.
+ */
+ public static final String CONTENT_LENGTH = "Content-Length";
+ /**
+ * The HTTP Content-Encoding header field name.
+ */
+ public static final String CONTENT_ENCODING = "Content-Encoding";
+ /**
+ * The HTTP Retry-After header field name.
+ */
+ public static final String RETRY_AFTER = "Retry-After";
+ /**
+ * Value for the Content-Encoding header that indicates that GZIP encoding is in use.
+ */
+ public static final String ENCODING_GZIP = "gzip";
+ /**
+ * Value for the Content-Encoding header that indicates that DEFLATE encoding is in use.
+ */
+ public static final String ENCODING_DEFLATE = "deflate";
+ /**
+ * UTF-8: eight-bit UCS Transformation Format.
+ */
+ public static final Charset UTF_8 = Charset.forName("UTF-8");
+
+ // com.google.common.base.Charsets
+ /**
+ * ISO-8859-1: ISO Latin Alphabet Number 1 (ISO-LATIN-1).
+ */
+ public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
+ private static final int BUF_SIZE = 0x800; // 2K chars (4K bytes)
+
+
+ /**
+ * Type literal for {@code Map}.
+ */
+ public static final Type MAP_STRING_WILDCARD =
+ new Types.ParameterizedTypeImpl(null, Map.class, String.class,
+ new Types.WildcardTypeImpl(new Type[]{Object.class}, new Type[0]));
+
+ private Util() { // no instances
+ }
+
+ /**
+ * Copy of {@code com.google.common.base.Preconditions#checkArgument}.
+ */
+ public static void checkArgument(boolean expression,
+ String errorMessageTemplate,
+ Object... errorMessageArgs) {
+ if (!expression) {
+ throw new IllegalArgumentException(
+ format(errorMessageTemplate, errorMessageArgs));
+ }
+ }
+
+ /**
+ * Copy of {@code com.google.common.base.Preconditions#checkNotNull}.
+ */
+ public static T checkNotNull(T reference,
+ String errorMessageTemplate,
+ Object... errorMessageArgs) {
+ if (reference == null) {
+ // If either of these parameters is null, the right thing happens anyway
+ throw new NullPointerException(
+ format(errorMessageTemplate, errorMessageArgs));
+ }
+ return reference;
+ }
+
+ /**
+ * Copy of {@code com.google.common.base.Preconditions#checkState}.
+ */
+ public static void checkState(boolean expression,
+ String errorMessageTemplate,
+ Object... errorMessageArgs) {
+ if (!expression) {
+ throw new IllegalStateException(
+ format(errorMessageTemplate, errorMessageArgs));
+ }
+ }
+
+ /**
+ * Identifies a method as a default instance method.
+ */
+ public static boolean isDefault(Method method) {
+ // Default methods are public non-abstract, non-synthetic, and non-static instance methods
+ // declared in an interface.
+ // method.isDefault() is not sufficient for our usage as it does not check
+ // for synthetic methods. As a result, it picks up overridden methods as well as actual default methods.
+ final int SYNTHETIC = 0x00001000;
+ return ((method.getModifiers() & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC | SYNTHETIC)) ==
+ Modifier.PUBLIC) && method.getDeclaringClass().isInterface();
+ }
+
+ /**
+ * Adapted from {@code com.google.common.base.Strings#emptyToNull}.
+ */
+ public static String emptyToNull(String string) {
+ return string == null || string.isEmpty() ? null : string;
+ }
+
+ /**
+ * Adapted from {@code com.google.common.base.Strings#emptyToNull}.
+ */
+ @SuppressWarnings("unchecked")
+ public static T[] toArray(Iterable extends T> iterable, Class type) {
+ Collection collection;
+ if (iterable instanceof Collection) {
+ collection = (Collection) iterable;
+ } else {
+ collection = new ArrayList();
+ for (T element : iterable) {
+ collection.add(element);
+ }
+ }
+ T[] array = (T[]) Array.newInstance(type, collection.size());
+ return collection.toArray(array);
+ }
+
+ /**
+ * Returns an unmodifiable collection which may be empty, but is never null.
+ */
+ public static Collection valuesOrEmpty(Map> map, String key) {
+ return map.containsKey(key) && map.get(key) != null ? map.get(key) : Collections.emptyList();
+ }
+
+ public static void ensureClosed(Closeable closeable) {
+ if (closeable != null) {
+ try {
+ closeable.close();
+ } catch (IOException ignored) { // NOPMD
+ }
+ }
+ }
+
+ /**
+ * Resolves the last type parameter of the parameterized {@code supertype}, based on the {@code
+ * genericContext}, into its upper bounds.
Implementation copied from {@code
+ * retrofit.RestMethodInfo}.
+ *
+ * @param genericContext Ex. {@link java.lang.reflect.Field#getGenericType()}
+ * @param supertype Ex. {@code Decoder.class}
+ * @return in the example above, the type parameter of {@code Decoder}.
+ * @throws IllegalStateException if {@code supertype} cannot be resolved into a parameterized type
+ * using {@code context}.
+ */
+ public static Type resolveLastTypeParameter(Type genericContext, Class> supertype)
+ throws IllegalStateException {
+ Type resolvedSuperType =
+ Types.getSupertype(genericContext, Types.getRawType(genericContext), supertype);
+ checkState(resolvedSuperType instanceof ParameterizedType,
+ "could not resolve %s into a parameterized type %s",
+ genericContext, supertype);
+ Type[] types = ParameterizedType.class.cast(resolvedSuperType).getActualTypeArguments();
+ for (int i = 0; i < types.length; i++) {
+ Type type = types[i];
+ if (type instanceof WildcardType) {
+ types[i] = ((WildcardType) type).getUpperBounds()[0];
+ }
+ }
+ return types[types.length - 1];
+ }
+
+ /**
+ * This returns well known empty values for well-known java types. This returns null for types not
+ * in the following list.
+ *
+ *
+ * {@code [Bb]oolean}
+ * {@code byte[]}
+ * {@code Collection}
+ * {@code Iterator}
+ * {@code List}
+ * {@code Map}
+ * {@code Set}
+ *
+ *
+ *
When {@link Feign.Builder#decode404() decoding HTTP 404 status}, you'll need to teach
+ * decoders a default empty value for a type. This method cheaply supports typical types by only
+ * looking at the raw type (vs type hierarchy). Decorate for sophistication.
+ */
+ public static Object emptyValueOf(Type type) {
+ return EMPTIES.get(Types.getRawType(type));
+ }
+
+ private static final Map, Object> EMPTIES;
+ static {
+ Map, Object> empties = new LinkedHashMap, Object>();
+ empties.put(boolean.class, false);
+ empties.put(Boolean.class, false);
+ empties.put(byte[].class, new byte[0]);
+ empties.put(Collection.class, Collections.emptyList());
+ empties.put(Iterator.class, new Iterator() { // Collections.emptyIterator is a 1.7 api
+ public boolean hasNext() {
+ return false;
+ }
+
+ public Object next() {
+ throw new NoSuchElementException();
+ }
+
+ public void remove() {
+ throw new IllegalStateException();
+ }
+ });
+ empties.put(List.class, Collections.emptyList());
+ empties.put(Map.class, Collections.emptyMap());
+ empties.put(Set.class, Collections.emptySet());
+ EMPTIES = Collections.unmodifiableMap(empties);
+ }
+
+ /**
+ * Adapted from {@code com.google.common.io.CharStreams.toString()}.
+ */
+ public static String toString(Reader reader) throws IOException {
+ if (reader == null) {
+ return null;
+ }
+ try {
+ StringBuilder to = new StringBuilder();
+ CharBuffer buf = CharBuffer.allocate(BUF_SIZE);
+ while (reader.read(buf) != -1) {
+ buf.flip();
+ to.append(buf);
+ buf.clear();
+ }
+ return to.toString();
+ } finally {
+ ensureClosed(reader);
+ }
+ }
+
+ /**
+ * Adapted from {@code com.google.common.io.ByteStreams.toByteArray()}.
+ */
+ public static byte[] toByteArray(InputStream in) throws IOException {
+ checkNotNull(in, "in");
+ try {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ copy(in, out);
+ return out.toByteArray();
+ } finally {
+ ensureClosed(in);
+ }
+ }
+
+ /**
+ * Adapted from {@code com.google.common.io.ByteStreams.copy()}.
+ */
+ private static long copy(InputStream from, OutputStream to)
+ throws IOException {
+ checkNotNull(from, "from");
+ checkNotNull(to, "to");
+ byte[] buf = new byte[BUF_SIZE];
+ long total = 0;
+ while (true) {
+ int r = from.read(buf);
+ if (r == -1) {
+ break;
+ }
+ to.write(buf, 0, r);
+ total += r;
+ }
+ return total;
+ }
+
+ public static String decodeOrDefault(byte[] data, Charset charset, String defaultValue) {
+ if (data == null) {
+ return defaultValue;
+ }
+ checkNotNull(charset, "charset");
+ try {
+ return charset.newDecoder().decode(ByteBuffer.wrap(data)).toString();
+ } catch (CharacterCodingException ex) {
+ return defaultValue;
+ }
+ }
+}
diff --git a/core/src/main/java/feign/auth/Base64.java b/core/src/main/java/feign/auth/Base64.java
new file mode 100644
index 0000000000..c565bc7c84
--- /dev/null
+++ b/core/src/main/java/feign/auth/Base64.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2013 Netflix, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package feign.auth;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ * copied from