diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000..4e740783fd
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,65 @@
+# Compiled source #
+###################
+*.com
+*.class
+*.dll
+*.exe
+*.o
+*.so
+
+# Packages #
+############
+# it's better to unpack these files and commit the raw source
+# git has its own built in compression methods
+*.7z
+*.dmg
+*.gz
+*.iso
+*.jar
+*.rar
+*.tar
+*.zip
+
+# Logs and databases #
+######################
+*.log
+
+# OS generated files #
+######################
+.DS_Store*
+ehthumbs.db
+Icon?
+Thumbs.db
+
+# Editor Files #
+################
+*~
+*.swp
+
+# Build output directies
+/target
+**/test-output
+**/target
+**/bin
+build
+*/build
+.m2
+
+# IntelliJ specific files/directories
+out
+.idea
+*.ipr
+*.iws
+*.iml
+atlassian-ide-plugin.xml
+
+# Eclipse specific files/directories
+.classpath
+.project
+.settings
+.metadata
+.factorypath
+.generated
+
+# NetBeans specific files/directories
+.nbattrs
diff --git a/.mvn/jvm.config b/.mvn/jvm.config
new file mode 100644
index 0000000000..0e7dabeff6
--- /dev/null
+++ b/.mvn/jvm.config
@@ -0,0 +1 @@
+-Xmx1024m -XX:CICompilerCount=1 -XX:TieredStopAtLevel=1 -Djava.security.egd=file:/dev/./urandom
\ No newline at end of file
diff --git a/.mvn/maven.config b/.mvn/maven.config
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/.mvn/maven.config
@@ -0,0 +1 @@
+
diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar
new file mode 100644
index 0000000000..c6feb8bb6f
Binary files /dev/null and b/.mvn/wrapper/maven-wrapper.jar differ
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 0000000000..56bb0164ec
--- /dev/null
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1 @@
+distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.0/apache-maven-3.5.0-bin.zip
\ No newline at end of file
diff --git a/.settings.xml b/.settings.xml
new file mode 100644
index 0000000000..110ded0f30
--- /dev/null
+++ b/.settings.xml
@@ -0,0 +1,43 @@
+
+
+
+
+ sonatype
+ ${env.SONATYPE_USER}
+ ${env.SONATYPE_PASSWORD}
+
+
+ bintray
+ ${env.BINTRAY_USER}
+ ${env.BINTRAY_KEY}
+
+
+ jfrog-snapshots
+ ${env.BINTRAY_USER}
+ ${env.BINTRAY_KEY}
+
+
+ github.com
+ ${env.GH_USER}
+ ${env.GH_TOKEN}
+
+
+
+
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000000..6a20ee9132
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,54 @@
+# Run `travis lint` when changing this file to avoid breaking the build.
+# Default JDK is really old: 1.8.0_31; Trusty's is less old: 1.8.0_51
+# https://docs.travis-ci.com/user/ci-environment/#Virtualization-environments
+sudo: required
+dist: trusty
+
+cache:
+ directories:
+ - $HOME/.m2
+
+language: java
+
+jdk:
+ - oraclejdk8
+
+
+before_install:
+ # Parameters used during release
+ - git config user.name "$GH_USER"
+ - git config user.email "$GH_USER_EMAIL"
+ # setup https authentication credentials, used by ./mvnw release:prepare
+ - git config credential.helper "store --file=.git/credentials"
+ - echo "https://$GH_TOKEN:@github.com" > .git/credentials
+
+install:
+ # Override default travis to use the maven wrapper
+ - ./mvnw install -DskipTests=true -Dmaven.javadoc.skip=true -B -V
+
+script:
+ - ./travis/publish.sh
+
+# Don't build release tags. This avoids publish conflicts because the version commit exists both on master and the release tag.
+# See https://github.com/travis-ci/travis-ci/issues/1532
+branches:
+ except:
+ - /^[0-9]/
+
+env:
+ global:
+ # Ex. travis encrypt BINTRAY_USER=your_github_account
+ - secure: "VeTOgXwhZLf8uwlnYpB9tuY+NV6kiooRN0FMDoWCXuPPSz/tX2mqmshBXDYsJu0EcRtZb21MkbQwbdJ8Th9K/bvj4sGNK1PrBm9Hmz6e2AvAcxn3ROv86GMTkd7O25OsipTT+/qWrbR3s3lHQxYo5WMsrlEmJ/EF5y5Go5wx90c="
+ # Ex. travis encrypt BINTRAY_KEY=xxx-https://bintray.com/profile/edit-xxx --add
+ - secure: "WND+fjAqpdHArSbXAK7l0dpQLrX0hL/XymV02rhe0pVT1g0J1V32ncqDCVnn/73wTiECTen6y3o1vq3ByIdT9tUErt3o8oEROQsI/cVX9IhvJ/DtcW1lqafXKmQZwDQsifVxhKroW1VuZQbGrKnqVUzfqx5OzxgoNVWpkkxhf50="
+ # Ex. travis encrypt GH_USER_EMAIL=for_github@domain.com --add
+ - secure: "KS/vYN2LZzIiFXVuPoStNG2343Jn7TzTEa3sWBlih075I8TNO1WUlGTzuQH/9xLRZ7wvjXYWQrQmPmA9jXEF6BCmVC3QYZPbXS/CR6L5O3EvFxX0oFE0NkUZ2ZiIIh1uRIjwIVqb715ktHO52XFZjEt69z97YQtS76CvRJtRKFI="
+ # Ex. travis encrypt GH_USER=your_github_account --add
+ - secure: "DW7Q0jChnosR9hBcugAeqfy48VFHRRDPOve1c1XVSmla7VgGgSDlIy8p/vTLEpquWHabRPSbkDisLBPcJxjyXnSx3EobNO8tcQXzQs45aRmcdLrmWOjJpmoNA3wQ6VQX9w9lKoXr6tBVyBuhQfX/QvOls1sRT/bSzstrffhHHv0="
+ # Ex. travis encrypt GH_TOKEN=XXX-https://github.com/settings/tokens-XXX --add
+ - secure: "bXCvr8DvpIbamiiR5XiEqyA6LIQWBmdKCpm0h5M4aUjmfpT18L5PcarxCu147l/yZiituitw4Ywz+nc4j4UKxtz9Oe84ouiDRZ2ynKZhUBOap3RWa7vOJ1Pj9sz20uSvyibX3R2b7lyOlk8PEhVywbhfWb6UE+bqMxQ10lgx6n8="
+ # Ex. travis encrypt SONATYPE_USER=your_sonatype_account
+ - secure: "ONAU76S0WBGcQGf0mr7KxKQjFvhhu73GNuQG8j47pxhJojNlNpWBbu+EGkgaInWKMtO89iBtpicVlXZc06HtbSqv7L93gbMo+xgp5daLlQg4gocDixjB1I2oPPITFFoztu76nOA1IBWRLTKu+w+Y2tKOmzWm+5v2UKD6fz7SYoo="
+ # Ex. travis encrypt SONATYPE_PASSWORD=your_sonatype_password
+ - secure: "UaVTxnw8klS36WLAdcmubqrHIgS4o5NcIqQMPIihk0tv3VEvCJSGvc2b7EPyQZMvm5TR3mXq5IJUAHp8j3seAHfYWmLIZWzvn7Y5mLRw8Kh9up7GzXl8Idui0AEHAAL2mfvE9smlOKPS5D13LKc6tOGFER66itHW3Jg1QoijDmQ="
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000000..0d7c2cf21f
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,262 @@
+### Version 9.6
+* Feign builder now supports flag `doNotCloseAfterDecode` to support lazy iteration of responses.
+* Adds `JacksonIteratorDecoder` and `StreamDecoder` to decode responses as `java.util.Iterator` or `java.util.stream.Stream`.
+
+### Version 9.5.1
+* When specified, Content-Type header is now included on OkHttp requests lacking a body.
+* Sets empty HttpEntity if apache request body is null.
+
+### Version 9.5
+* Introduces `feign-java8` with support for `java.util.Optional`
+* Adds `Feign.Builder.mapAndDecode()` to allow response preprocessing before decoding it.
+
+### Version 9.4.1
+* 404 responses are no longer swallowed for `void` return types.
+
+### Version 9.4
+* Adds Builder class to JAXBDecoder for disabling namespace-awareness (defaults to true).
+
+### Version 9.3
+* Adds `FallbackFactory`, allowing access to the cause of a Hystrix fallback
+* Adds support for encoded parameters via `@Param(encoded = true)`
+
+### Version 9.2
+* Adds Hystrix `SetterFactory` to customize group and command keys
+* Supports context path when using Ribbon `LoadBalancingTarget`
+* Adds builder methods for the Response object
+* Deprecates Response factory methods
+* Adds nullable Request field to the Response object
+
+### Version 9.1
+* Allows query parameters to match on a substring. Ex `q=body:{body}`
+
+### Version 9.0
+* Migrates to maven from gradle
+* Changes maven groupId to `io.github.openfeign`
+
+### Version 8.18
+* Adds support for expansion of @Param lists
+* Content-Length response bodies with lengths greater than Integer.MAX_VALUE report null length
+ * Previously the OkhttpClient would throw an exception, and ApacheHttpClient
+ would report a wrong, possibly negative value
+* Adds support for encoded query parameters in `@QueryMap` via `@QueryMap(encoded = true)`
+* Keys in `Response.headers` are now lower-cased. This map is now case-insensitive with regards to keys,
+ and iterates in lexicographic order.
+ * This is a step towards supporting http2, as header names in http1 are treated as case-insensitive
+ and http2 down-cases header names.
+
+### Version 8.17
+* Adds support to RxJava Completable via `HystrixFeign` builder with fallback support
+* Upgraded hystrix-core to 1.4.26
+* Upgrades dependency version for OkHttp/MockWebServer 3.2.0
+
+### Version 8.16
+* Adds `@HeaderMap` annotation to support dynamic header fields and values
+* Add support for default and static methods on interfaces
+
+### Version 8.15
+* Adds `@QueryMap` annotation to support dynamic query parameters
+* Supports runtime injection of `Param.Expander` via `MethodMetadata.indexToExpander`
+* Adds fallback support for HystrixCommand, Observable, and Single results
+* Supports PUT without a body parameter
+* Supports substitutions in `@Headers` like in `@Body`. (#326)
+ * **Note:** You might need to URL-encode literal values of `{` or `%` in your existing code.
+
+### Version 8.14
+* Add support for RxJava Observable and Single return types via the `HystrixFeign` builder.
+* Adds fallback implementation configuration to the `HystrixFeign` builder
+* Bumps dependency versions, most notably Gson 2.5 and OkHttp 2.7
+
+### Version 8.13
+* Never expands >8kb responses into memory
+
+### Version 8.12
+* Adds `Feign.Builder.decode404()` to reduce boilerplate for empty semantics.
+
+### Version 8.11
+* Adds support for Hystrix via a `HystrixFeign` builder.
+
+### Version 8.10
+* Adds HTTP status to FeignException for easier response handling
+* Reads class-level @Produces/@Consumes JAX-RS annotations
+* Supports POST without a body parameter
+
+### Version 8.9
+* Skips error handling when return type is `Response`
+
+### Version 8.8
+* Adds jackson-jaxb codec
+* Bumps dependency versions for integrations
+ * OkHttp/MockWebServer 2.5.0
+ * Jackson 2.6.1
+ * Apache Http Client 4.5
+ * JMH 1.10.5
+
+### Version 8.7
+* Bumps dependency versions for integrations
+ * OkHttp/MockWebServer 2.4.0
+ * Gson 2.3.1
+ * Jackson 2.6.0
+ * Ribbon 2.1.0
+ * SLF4J 1.7.12
+
+### Version 8.6
+* Adds base api support via single-inheritance interfaces
+
+### Version 7.5/8.5
+* Added possibility to leave slash encoded in path parameters
+
+### Version 8.4
+* Correct Retryer bug that prevented it from retrying requests after the first 5 retry attempts.
+ * **Note:** If you have a custom `feign.Retryer` implementation you now must now implement `public Retryer clone()`.
+ It is suggested that you simply return a new instance of your Retryer class.
+
+### Version 8.3
+* Adds client implementation for Apache Http Client
+
+### Version 8.2
+* Allows customized request construction by exposing `Request.create()`
+* Adds JMH benchmark module
+* Enforces source compatibility with animal-sniffer
+
+### Version 8.1
+* Allows `@Headers` to be applied to a type
+
+### Version 8.0
+* Removes Dagger 1.x Dependency
+* Removes support for parameters annotated with `javax.inject.@Named`. Use `feign.@Param` instead.
+* Makes body parameter type explicit.
+
+### Version 7.4
+* Allows `@Headers` to be applied to a type
+
+### Version 7.3
+* Adds Request.Options support to RibbonClient
+* Adds LBClientFactory to enable caching of Ribbon LBClients
+* Updates to Ribbon 2.0-RC13
+* Updates to Jackson 2.5.1
+* Supports query parameters without values
+
+### Version 7.2
+* Adds `Feign.Builder.build()`
+* Opens constructor for Gson and Jackson codecs which accepts type adapters
+* Adds EmptyTarget for interfaces who exclusively declare URI methods
+* Reformats code according to [Google Java Style](https://google-styleguide.googlecode.com/svn/trunk/javaguide.html)
+
+### Version 7.1
+* Introduces feign.@Param to annotate template parameters. Users must migrate from `javax.inject.@Named` to `feign.@Param` before updating to Feign 8.0.
+ * Supports custom expansion via `@Param(value = "name", expander = CustomExpander.class)`
+* Adds OkHttp integration
+* Allows multiple headers with the same name.
+* Ensures Accept headers default to `*/*`
+
+### Version 7.0
+* Expose reflective dispatch hook: InvocationHandlerFactory
+* Add JAXB integration
+* Add SLF4J integration
+* Upgrade to Dagger 1.2.2.
+ * **Note:** Dagger-generated code prior to version 1.2.0 is incompatible with Dagger 1.2.0 and beyond. Dagger users should upgrade Dagger to at least version 1.2.0, and recompile any dependency-injected classes.
+
+### Version 6.1.3
+* Updates to Ribbon 2.0-RC5
+
+### Version 6.1.1
+* Fix for #85
+
+### Version 6.1.0
+* Add [SLF4J](http://www.slf4j.org/) integration
+
+### Version 6.0.1
+* Fix for BasicAuthRequestInterceptor when username and/or password are long.
+
+### Version 6.0
+* Support binary request and response bodies.
+* Don't throw http status code exceptions when return type is `Response`.
+
+### Version 5.4.0
+* Add `BasicAuthRequestInterceptor`
+* Add Jackson integration
+
+### Version 5.3.0
+* Split `GsonCodec` into `GsonEncoder` and `GsonDecoder`, which are easy to use with `Feign.Builder`
+* Deprecate `GsonCodec`
+* Update to Ribbon 0.2.3
+
+### Version 5.2.0
+* Support usage of `GsonCodec` via `Feign.Builder`
+
+### Version 5.1.0
+* Correctly handle IOExceptions wrapped by Ribbon.
+* Miscellaneous findbugs fixes.
+
+### Version 5.0.1
+* `Decoder.decode()` is no longer called for `Response` or `void` types.
+
+### Version 5.0
+* Remove support for Observable methods.
+* Use single non-generic Decoder/Encoder instead of sets of type-specific Decoders/Encoders.
+* Decoders/Encoders are now more flexible, having access to the Response/RequestTemplate respectively.
+* Moved SaxDecoder into `feign-sax` dependency.
+ * SaxDecoder now decodes multiple types.
+ * Remove pattern decoders in favor of SaxDecoder.
+* Added Feign.Builder to simplify client customizations without using Dagger.
+* Gson type adapters can be registered as Dagger set bindings.
+* `Feign.create(...)` now requires specifying an encoder and decoder.
+
+### Version 4.4.1
+* Fix NullPointerException on calling equals and hashCode.
+
+### Version 4.4
+* Support overriding default HostnameVerifier.
+* Support GZIP content encoding for request bodies.
+* Support Iterable args for query parameters.
+* Support urls which have query parameters.
+
+### Version 4.3
+* Add ability to configure zero or more RequestInterceptors.
+* Remove `overrides = true` on codec modules.
+
+### Version 4.2/3.3
+* Document and enforce JAX-RS annotation processing from server POV
+* Skip query template parameters when corresponding java arg is null
+
+### Version 4.1/3.2
+* update to dagger 1.1
+* Add wikipedia search example
+* Allow `@Path` on types in feign-jaxrs
+
+### Version 4.0
+* Support RxJava-style Observers.
+ * Return type can be `Observable` for an async equiv of `Iterable`.
+ * `Observer` replaces `IncrementalCallback` and is passed to `Observable.subscribe()`.
+ * On `Subscription.unsubscribe()`, `Observer.onNext()` will stop being called.
+
+### Version 3.1
+* Log when an http request is retried or a response fails due to an IOException.
+
+### Version 3.0
+* Added support for asynchronous callbacks via `IncrementalCallback` and `IncrementalDecoder.TextStream`.
+* Wire is now Logger, with configurable Logger.Level.
+* Added `feign-gson` codec, used via `new GsonModule()`
+* changed codec to be similar to [WebSocket JSR 356](http://docs.oracle.com/javaee/7/api/javax/websocket/package-summary.html)
+ * Decoder is now `Decoder.TextStream`
+ * BodyEncoder is now `Encoder.Text`
+ * FormEncoder is now `Encoder.Text
Feign standard decoders do not have built in support for this flag. If
+ * you are using this flag, you MUST also use a custom Decoder, and be sure to
+ * close all resources appropriately somewhere in the Decoder (you can use
+ * {@link Util.ensureClosed} for convenience).
+ *
+ * @since 9.6
+ *
+ */
+ public Builder doNotCloseAfterDecode() {
+ this.closeAfterDecode = false;
+ return this;
+ }
+
+ public T target(Class apiType, String url) {
+ return target(new HardCodedTarget(apiType, url));
+ }
+
+ public T target(Target target) {
+ return build().newInstance(target);
+ }
+
+ public Feign build() {
+ SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
+ new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
+ logLevel, decode404, closeAfterDecode);
+ ParseHandlersByName handlersByName =
+ new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,
+ errorDecoder, synchronousMethodHandlerFactory);
+ return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
+ }
+ }
+
+ static class ResponseMappingDecoder implements Decoder {
+
+ private final ResponseMapper mapper;
+ private final Decoder delegate;
+
+ ResponseMappingDecoder(ResponseMapper mapper, Decoder decoder) {
+ this.mapper = mapper;
+ this.delegate = decoder;
+ }
+
+ @Override
+ public Object decode(Response response, Type type) throws IOException {
+ return delegate.decode(mapper.map(response, type), type);
+ }
+ }
+}
diff --git a/core/src/main/java/feign/FeignException.java b/core/src/main/java/feign/FeignException.java
new file mode 100644
index 0000000000..794d02b28e
--- /dev/null
+++ b/core/src/main/java/feign/FeignException.java
@@ -0,0 +1,68 @@
+/**
+ * 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
+ * the License.
+ */
+package feign;
+
+import java.io.IOException;
+
+import static java.lang.String.format;
+
+/**
+ * Origin exception type for all Http Apis.
+ */
+public class FeignException extends RuntimeException {
+
+ private static final long serialVersionUID = 0;
+ private int status;
+
+ protected FeignException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ protected FeignException(String message) {
+ super(message);
+ }
+
+ protected FeignException(int status, String message) {
+ super(message);
+ this.status = status;
+ }
+
+ public int status() {
+ return this.status;
+ }
+
+ static FeignException errorReading(Request request, Response ignored, IOException cause) {
+ return new FeignException(
+ format("%s reading %s %s", cause.getMessage(), request.method(), request.url()),
+ cause);
+ }
+
+ public static FeignException errorStatus(String methodKey, Response response) {
+ String message = format("status %s reading %s", response.status(), methodKey);
+ try {
+ if (response.body() != null) {
+ String body = Util.toString(response.body().asReader());
+ message += "; content:\n" + body;
+ }
+ } catch (IOException ignored) { // NOPMD
+ }
+ return new FeignException(response.status(), message);
+ }
+
+ static FeignException errorExecuting(Request request, IOException cause) {
+ return new RetryableException(
+ format("%s executing %s %s", cause.getMessage(), request.method(), request.url()), cause,
+ null);
+ }
+}
diff --git a/core/src/main/java/feign/HeaderMap.java b/core/src/main/java/feign/HeaderMap.java
new file mode 100644
index 0000000000..539dddd011
--- /dev/null
+++ b/core/src/main/java/feign/HeaderMap.java
@@ -0,0 +1,68 @@
+/**
+ * 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
+ * the License.
+ */
+package feign;
+
+import java.lang.annotation.Retention;
+import java.util.List;
+import java.util.Map;
+
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * A template parameter that can be applied to a Map that contains header
+ * entries, where the keys are Strings that are the header field names and the
+ * values are the header field values. The headers specified by the map will be
+ * applied to the request after all other processing, and will take precedence
+ * over any previously specified header parameters.
+ *
+ * This parameter is useful in cases where different header fields and values
+ * need to be set on an API method on a per-request basis in a thread-safe manner
+ * and independently of Feign client construction. A concrete example of a case
+ * like this are custom metadata header fields (e.g. as "x-amz-meta-*" or
+ * "x-goog-meta-*") where the header field names are dynamic and the range of keys
+ * cannot be determined a priori. The {@link Headers} annotation does not allow this
+ * because the header fields that it defines are static (it is not possible to add or
+ * remove fields on a per-request basis), and doing this using a custom {@link Target}
+ * or {@link RequestInterceptor} can be cumbersome (it requires more code for per-method
+ * customization, it is difficult to implement in a thread-safe manner and it requires
+ * customization when the Feign client for the API is built).
+ *
+ *
+ * The annotated parameter must be an instance of {@link Map}, and the keys must
+ * be Strings. The header field value of a key will be the value of its toString
+ * method, except in the following cases:
+ *
+ *
+ *
+ *
if the value is null, the value will remain null (rather than converting
+ * to the String "null")
+ *
if the value is an {@link Iterable}, it is converted to a {@link List} of
+ * String objects where each value in the list is either null if the original
+ * value was null or the value's toString representation otherwise.
+ *
+ *
+ * Once this conversion is applied, the query keys and resulting String values
+ * follow the same contract as if they were set using
+ * {@link RequestTemplate#header(String, String...)}.
+ */
+@Retention(RUNTIME)
+@java.lang.annotation.Target(PARAMETER)
+public @interface HeaderMap {
+}
diff --git a/core/src/main/java/feign/Headers.java b/core/src/main/java/feign/Headers.java
new file mode 100644
index 0000000000..75552bca9e
--- /dev/null
+++ b/core/src/main/java/feign/Headers.java
@@ -0,0 +1,66 @@
+/**
+ * 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
+ * the License.
+ */
+package feign;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * Expands headers supplied in the {@code value}. Variables to the the right of the colon are expanded.
+ *
+ * The annotated parameter must be an instance of {@link Map}, and the keys must
+ * be Strings. The query value of a key will be the value of its toString
+ * method, except in the following cases:
+ *
+ *
+ *
+ *
if the value is null, the value will remain null (rather than converting
+ * to the String "null")
+ *
if the value is an {@link Iterable}, it is converted to a {@link List} of
+ * String objects where each value in the list is either null if the original
+ * value was null or the value's toString representation otherwise.
+ *
+ *
+ * Once this conversion is applied, the query keys and resulting String values
+ * follow the same contract as if they were set using
+ * {@link RequestTemplate#query(String, String...)}.
+ */
+@Retention(RUNTIME)
+@java.lang.annotation.Target(PARAMETER)
+public @interface QueryMap {
+ /**
+ * Specifies whether parameter names and values are already encoded.
+ *
+ * @see Param#encoded
+ */
+ boolean encoded() default false;
+}
diff --git a/core/src/main/java/feign/QueryMapEncoder.java b/core/src/main/java/feign/QueryMapEncoder.java
new file mode 100644
index 0000000000..b6909823b4
--- /dev/null
+++ b/core/src/main/java/feign/QueryMapEncoder.java
@@ -0,0 +1,88 @@
+/**
+ * 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
+ * the License.
+ */
+package feign;
+
+import feign.codec.EncodeException;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A QueryMapEncoder encodes Objects into maps of query parameter names to values.
+ */
+public interface QueryMapEncoder {
+
+ /**
+ * Encodes the given object into a query map.
+ *
+ * @param object the object to encode
+ * @return the map represented by the object
+ */
+ Map encode (Object object);
+
+ class Default implements QueryMapEncoder {
+
+ private final Map, ObjectParamMetadata> classToMetadata =
+ new HashMap, ObjectParamMetadata>();
+
+ @Override
+ public Map encode (Object object) throws EncodeException {
+ try {
+ ObjectParamMetadata metadata = getMetadata(object.getClass());
+ Map fieldNameToValue = new HashMap();
+ for (Field field : metadata.objectFields) {
+ Object value = field.get(object);
+ if (value != null && value != object) {
+ fieldNameToValue.put(field.getName(), value);
+ }
+ }
+ return fieldNameToValue;
+ } catch (IllegalAccessException e) {
+ throw new EncodeException("Failure encoding object into query map", e);
+ }
+ }
+
+ private ObjectParamMetadata getMetadata(Class> objectType) {
+ ObjectParamMetadata metadata = classToMetadata.get(objectType);
+ if (metadata == null) {
+ metadata = ObjectParamMetadata.parseObjectType(objectType);
+ classToMetadata.put(objectType, metadata);
+ }
+ return metadata;
+ }
+
+ private static class ObjectParamMetadata {
+
+ private final List objectFields;
+
+ private ObjectParamMetadata (List objectFields) {
+ this.objectFields = Collections.unmodifiableList(objectFields);
+ }
+
+ private static ObjectParamMetadata parseObjectType(Class> type) {
+ List fields = new ArrayList();
+ for (Field field : type.getDeclaredFields()) {
+ if (!field.isAccessible()) {
+ field.setAccessible(true);
+ }
+ fields.add(field);
+ }
+ return new ObjectParamMetadata(fields);
+ }
+ }
+ }
+}
diff --git a/core/src/main/java/feign/ReflectiveFeign.java b/core/src/main/java/feign/ReflectiveFeign.java
new file mode 100644
index 0000000000..91332e8671
--- /dev/null
+++ b/core/src/main/java/feign/ReflectiveFeign.java
@@ -0,0 +1,377 @@
+/**
+ * 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
+ * the License.
+ */
+package feign;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.*;
+import java.util.Map.Entry;
+
+import feign.InvocationHandlerFactory.MethodHandler;
+import feign.Param.Expander;
+import feign.Request.Options;
+import feign.codec.Decoder;
+import feign.codec.EncodeException;
+import feign.codec.Encoder;
+import feign.codec.ErrorDecoder;
+
+import static feign.Util.checkArgument;
+import static feign.Util.checkNotNull;
+
+public class ReflectiveFeign extends Feign {
+
+ private final ParseHandlersByName targetToHandlersByName;
+ private final InvocationHandlerFactory factory;
+ private final QueryMapEncoder queryMapEncoder;
+
+ ReflectiveFeign(ParseHandlersByName targetToHandlersByName, InvocationHandlerFactory factory, QueryMapEncoder queryMapEncoder) {
+ this.targetToHandlersByName = targetToHandlersByName;
+ this.factory = factory;
+ this.queryMapEncoder = queryMapEncoder;
+ }
+
+ /**
+ * creates an api binding to the {@code target}. As this invokes reflection, care should be taken
+ * to cache the result.
+ */
+ @SuppressWarnings("unchecked")
+ @Override
+ public T newInstance(Target target) {
+ Map nameToHandler = targetToHandlersByName.apply(target);
+ Map methodToHandler = new LinkedHashMap();
+ List defaultMethodHandlers = new LinkedList();
+
+ for (Method method : target.type().getMethods()) {
+ if (method.getDeclaringClass() == Object.class) {
+ continue;
+ } else if(Util.isDefault(method)) {
+ DefaultMethodHandler handler = new DefaultMethodHandler(method);
+ defaultMethodHandlers.add(handler);
+ methodToHandler.put(method, handler);
+ } else {
+ methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
+ }
+ }
+ InvocationHandler handler = factory.create(target, methodToHandler);
+ T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class>[]{target.type()}, handler);
+
+ for(DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
+ defaultMethodHandler.bindTo(proxy);
+ }
+ return proxy;
+ }
+
+ static class FeignInvocationHandler implements InvocationHandler {
+
+ private final Target target;
+ private final Map dispatch;
+
+ FeignInvocationHandler(Target target, Map dispatch) {
+ this.target = checkNotNull(target, "target");
+ this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);
+ }
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ if ("equals".equals(method.getName())) {
+ try {
+ Object
+ otherHandler =
+ args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
+ return equals(otherHandler);
+ } catch (IllegalArgumentException e) {
+ return false;
+ }
+ } else if ("hashCode".equals(method.getName())) {
+ return hashCode();
+ } else if ("toString".equals(method.getName())) {
+ return toString();
+ }
+ return dispatch.get(method).invoke(args);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof FeignInvocationHandler) {
+ FeignInvocationHandler other = (FeignInvocationHandler) obj;
+ return target.equals(other.target);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return target.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return target.toString();
+ }
+ }
+
+ static final class ParseHandlersByName {
+
+ private final Contract contract;
+ private final Options options;
+ private final Encoder encoder;
+ private final Decoder decoder;
+ private final ErrorDecoder errorDecoder;
+ private final QueryMapEncoder queryMapEncoder;
+ private final SynchronousMethodHandler.Factory factory;
+
+ ParseHandlersByName(
+ Contract contract,
+ Options options,
+ Encoder encoder,
+ Decoder decoder,
+ QueryMapEncoder queryMapEncoder,
+ ErrorDecoder errorDecoder,
+ SynchronousMethodHandler.Factory factory) {
+ this.contract = contract;
+ this.options = options;
+ this.factory = factory;
+ this.errorDecoder = errorDecoder;
+ this.queryMapEncoder = queryMapEncoder;
+ this.encoder = checkNotNull(encoder, "encoder");
+ this.decoder = checkNotNull(decoder, "decoder");
+ }
+
+ public Map apply(Target key) {
+ List metadata = contract.parseAndValidatateMetadata(key.type());
+ Map result = new LinkedHashMap();
+ for (MethodMetadata md : metadata) {
+ BuildTemplateByResolvingArgs buildTemplate;
+ if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) {
+ buildTemplate = new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder);
+ } else if (md.bodyIndex() != null) {
+ buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder);
+ } else {
+ buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder);
+ }
+ result.put(md.configKey(),
+ factory.create(key, md, buildTemplate, options, decoder, errorDecoder));
+ }
+ return result;
+ }
+ }
+
+ private static class BuildTemplateByResolvingArgs implements RequestTemplate.Factory {
+
+ private final QueryMapEncoder queryMapEncoder;
+
+ protected final MethodMetadata metadata;
+ private final Map indexToExpander = new LinkedHashMap();
+
+ private BuildTemplateByResolvingArgs(MethodMetadata metadata, QueryMapEncoder queryMapEncoder) {
+ this.metadata = metadata;
+ this.queryMapEncoder = queryMapEncoder;
+ if (metadata.indexToExpander() != null) {
+ indexToExpander.putAll(metadata.indexToExpander());
+ return;
+ }
+ if (metadata.indexToExpanderClass().isEmpty()) {
+ return;
+ }
+ for (Entry> indexToExpanderClass : metadata
+ .indexToExpanderClass().entrySet()) {
+ try {
+ indexToExpander
+ .put(indexToExpanderClass.getKey(), indexToExpanderClass.getValue().newInstance());
+ } catch (InstantiationException e) {
+ throw new IllegalStateException(e);
+ } catch (IllegalAccessException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ }
+
+ @Override
+ public RequestTemplate create(Object[] argv) {
+ RequestTemplate mutable = new RequestTemplate(metadata.template());
+ if (metadata.urlIndex() != null) {
+ int urlIndex = metadata.urlIndex();
+ checkArgument(argv[urlIndex] != null, "URI parameter %s was null", urlIndex);
+ mutable.insert(0, String.valueOf(argv[urlIndex]));
+ }
+ Map varBuilder = new LinkedHashMap();
+ for (Entry> entry : metadata.indexToName().entrySet()) {
+ int i = entry.getKey();
+ Object value = argv[entry.getKey()];
+ if (value != null) { // Null values are skipped.
+ if (indexToExpander.containsKey(i)) {
+ value = expandElements(indexToExpander.get(i), value);
+ }
+ for (String name : entry.getValue()) {
+ varBuilder.put(name, value);
+ }
+ }
+ }
+
+ RequestTemplate template = resolve(argv, mutable, varBuilder);
+ if (metadata.queryMapIndex() != null) {
+ // add query map parameters after initial resolve so that they take
+ // precedence over any predefined values
+ Object value = argv[metadata.queryMapIndex()];
+ Map queryMap = toQueryMap(value);
+ template = addQueryMapQueryParameters(queryMap, template);
+ }
+
+ if (metadata.headerMapIndex() != null) {
+ template = addHeaderMapHeaders((Map) argv[metadata.headerMapIndex()], template);
+ }
+
+ return template;
+ }
+
+ private Map toQueryMap (Object value) {
+ if (value instanceof Map) {
+ return (Map)value;
+ }
+ try {
+ return queryMapEncoder.encode(value);
+ } catch (EncodeException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private Object expandElements(Expander expander, Object value) {
+ if (value instanceof Iterable) {
+ return expandIterable(expander, (Iterable) value);
+ }
+ return expander.expand(value);
+ }
+
+ private List expandIterable(Expander expander, Iterable value) {
+ List values = new ArrayList();
+ for (Object element : value) {
+ if (element!=null) {
+ values.add(expander.expand(element));
+ }
+ }
+ return values;
+ }
+
+ @SuppressWarnings("unchecked")
+ private RequestTemplate addHeaderMapHeaders(Map headerMap, RequestTemplate mutable) {
+ for (Entry currEntry : headerMap.entrySet()) {
+ Collection values = new ArrayList();
+
+ Object currValue = currEntry.getValue();
+ if (currValue instanceof Iterable>) {
+ Iterator> iter = ((Iterable>) currValue).iterator();
+ while (iter.hasNext()) {
+ Object nextObject = iter.next();
+ values.add(nextObject == null ? null : nextObject.toString());
+ }
+ } else {
+ values.add(currValue == null ? null : currValue.toString());
+ }
+
+ mutable.header(currEntry.getKey(), values);
+ }
+ return mutable;
+ }
+
+ @SuppressWarnings("unchecked")
+ private RequestTemplate addQueryMapQueryParameters(Map queryMap, RequestTemplate mutable) {
+ for (Entry currEntry : queryMap.entrySet()) {
+ Collection values = new ArrayList();
+
+ boolean encoded = metadata.queryMapEncoded();
+ Object currValue = currEntry.getValue();
+ if (currValue instanceof Iterable>) {
+ Iterator> iter = ((Iterable>) currValue).iterator();
+ while (iter.hasNext()) {
+ Object nextObject = iter.next();
+ values.add(nextObject == null ? null : encoded ? nextObject.toString() : RequestTemplate.urlEncode(nextObject.toString()));
+ }
+ } else {
+ values.add(currValue == null ? null : encoded ? currValue.toString() : RequestTemplate.urlEncode(currValue.toString()));
+ }
+
+ mutable.query(true, encoded ? currEntry.getKey() : RequestTemplate.urlEncode(currEntry.getKey()), values);
+ }
+ return mutable;
+ }
+
+ protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable,
+ Map variables) {
+ // Resolving which variable names are already encoded using their indices
+ Map variableToEncoded = new LinkedHashMap();
+ for (Entry entry : metadata.indexToEncoded().entrySet()) {
+ Collection names = metadata.indexToName().get(entry.getKey());
+ for (String name : names) {
+ variableToEncoded.put(name, entry.getValue());
+ }
+ }
+ return mutable.resolve(variables, variableToEncoded);
+ }
+ }
+
+ private static class BuildFormEncodedTemplateFromArgs extends BuildTemplateByResolvingArgs {
+
+ private final Encoder encoder;
+
+ private BuildFormEncodedTemplateFromArgs(MethodMetadata metadata, Encoder encoder, QueryMapEncoder queryMapEncoder) {
+ super(metadata, queryMapEncoder);
+ this.encoder = encoder;
+ }
+
+ @Override
+ protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable,
+ Map variables) {
+ Map formVariables = new LinkedHashMap();
+ for (Entry entry : variables.entrySet()) {
+ if (metadata.formParams().contains(entry.getKey())) {
+ formVariables.put(entry.getKey(), entry.getValue());
+ }
+ }
+ try {
+ encoder.encode(formVariables, Encoder.MAP_STRING_WILDCARD, mutable);
+ } catch (EncodeException e) {
+ throw e;
+ } catch (RuntimeException e) {
+ throw new EncodeException(e.getMessage(), e);
+ }
+ return super.resolve(argv, mutable, variables);
+ }
+ }
+
+ private static class BuildEncodedTemplateFromArgs extends BuildTemplateByResolvingArgs {
+
+ private final Encoder encoder;
+
+ private BuildEncodedTemplateFromArgs(MethodMetadata metadata, Encoder encoder, QueryMapEncoder queryMapEncoder) {
+ super(metadata, queryMapEncoder);
+ this.encoder = encoder;
+ }
+
+ @Override
+ protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable,
+ Map variables) {
+ Object body = argv[metadata.bodyIndex()];
+ checkArgument(body != null, "Body parameter %s was null", metadata.bodyIndex());
+ try {
+ encoder.encode(body, metadata.bodyType(), mutable);
+ } catch (EncodeException e) {
+ throw e;
+ } catch (RuntimeException e) {
+ throw new EncodeException(e.getMessage(), e);
+ }
+ return super.resolve(argv, mutable, variables);
+ }
+ }
+}
diff --git a/core/src/main/java/feign/Request.java b/core/src/main/java/feign/Request.java
new file mode 100644
index 0000000000..8890d8102e
--- /dev/null
+++ b/core/src/main/java/feign/Request.java
@@ -0,0 +1,151 @@
+/**
+ * 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
+ * the License.
+ */
+package feign;
+
+import java.net.HttpURLConnection;
+import java.nio.charset.Charset;
+import java.util.Collection;
+import java.util.Map;
+
+import static feign.Util.checkNotNull;
+import static feign.Util.valuesOrEmpty;
+
+/**
+ * An immutable request to an http server.
+ */
+public final class Request {
+
+ /**
+ * No parameters can be null except {@code body} and {@code charset}. All parameters must be
+ * effectively immutable, via safe copies, not mutating or otherwise.
+ */
+ public static Request create(String method, String url, Map> headers,
+ byte[] body, Charset charset) {
+ return new Request(method, url, headers, body, charset);
+ }
+
+ private final String method;
+ private final String url;
+ private final Map> headers;
+ private final byte[] body;
+ private final Charset charset;
+
+ Request(String method, String url, Map> headers, byte[] body,
+ Charset charset) {
+ this.method = checkNotNull(method, "method of %s", url);
+ this.url = checkNotNull(url, "url");
+ this.headers = checkNotNull(headers, "headers of %s %s", method, url);
+ this.body = body; // nullable
+ this.charset = charset; // nullable
+ }
+
+ /* Method to invoke on the server. */
+ public String method() {
+ return method;
+ }
+
+ /* Fully resolved URL including query. */
+ public String url() {
+ return url;
+ }
+
+ /* Ordered list of headers that will be sent to the server. */
+ public Map> 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;
+ private final boolean followRedirects;
+
+ public Options(int connectTimeoutMillis, int readTimeoutMillis, boolean followRedirects) {
+ this.connectTimeoutMillis = connectTimeoutMillis;
+ this.readTimeoutMillis = readTimeoutMillis;
+ this.followRedirects = followRedirects;
+ }
+
+ public Options(int connectTimeoutMillis, int readTimeoutMillis){
+ this(connectTimeoutMillis, readTimeoutMillis, true);
+ }
+
+ 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;
+ }
+
+
+ /**
+ * Defaults to true. {@code false} tells the client to not follow the redirections.
+ *
+ * @see HttpURLConnection#getFollowRedirects()
+ */
+ public boolean isFollowRedirects() {
+ return followRedirects;
+ }
+ }
+}
diff --git a/core/src/main/java/feign/RequestInterceptor.java b/core/src/main/java/feign/RequestInterceptor.java
new file mode 100644
index 0000000000..8e8deb219b
--- /dev/null
+++ b/core/src/main/java/feign/RequestInterceptor.java
@@ -0,0 +1,42 @@
+/**
+ * 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
+ * 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)}.
{@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..076506c104
--- /dev/null
+++ b/core/src/main/java/feign/RequestLine.java
@@ -0,0 +1,65 @@
+/**
+ * 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
+ * the License.
+ */
+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.
+ *
+ */
+@java.lang.annotation.Target(METHOD)
+@Retention(RUNTIME)
+public @interface RequestLine {
+
+ String value();
+ boolean decodeSlash() default true;
+ CollectionFormat collectionFormat() default CollectionFormat.EXPLODED;
+}
diff --git a/core/src/main/java/feign/RequestTemplate.java b/core/src/main/java/feign/RequestTemplate.java
new file mode 100644
index 0000000000..35752bbd7e
--- /dev/null
+++ b/core/src/main/java/feign/RequestTemplate.java
@@ -0,0 +1,683 @@
+/**
+ * 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
+ * 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;
+ private CollectionFormat collectionFormat = CollectionFormat.EXPLODED;
+
+ 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;
+ this.collectionFormat = toCopy.collectionFormat;
+ }
+
+ 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;
+ }
+
+ public RequestTemplate collectionFormat(CollectionFormat collectionFormat) {
+ this.collectionFormat = collectionFormat;
+ return this;
+ }
+
+ public CollectionFormat collectionFormat() {
+ return collectionFormat;
+ }
+
+ /* @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.
+ *
+ * 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.
+ *
+ */
+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..e3e912f744
--- /dev/null
+++ b/core/src/main/java/feign/RetryableException.java
@@ -0,0 +1,51 @@
+/**
+ * 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
+ * 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..0cacec3ffa
--- /dev/null
+++ b/core/src/main/java/feign/Retryer.java
@@ -0,0 +1,114 @@
+/**
+ * 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
+ * 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();
+ throw e;
+ }
+ 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..01e4a2fed4
--- /dev/null
+++ b/core/src/main/java/feign/SynchronousMethodHandler.java
@@ -0,0 +1,205 @@
+/**
+ * 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
+ * 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 final boolean closeAfterDecode;
+
+ 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,
+ boolean closeAfterDecode) {
+ 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;
+ this.closeAfterDecode = closeAfterDecode;
+ }
+
+ @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 {
+ shouldClose = closeAfterDecode;
+ return decode(response);
+ }
+ } else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) {
+ shouldClose = closeAfterDecode;
+ 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;
+ private final boolean closeAfterDecode;
+
+ Factory(Client client, Retryer retryer, List requestInterceptors,
+ Logger logger, Logger.Level logLevel, boolean decode404, boolean closeAfterDecode) {
+ 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;
+ this.closeAfterDecode = closeAfterDecode;
+ }
+
+ 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, closeAfterDecode);
+ }
+ }
+}
diff --git a/core/src/main/java/feign/Target.java b/core/src/main/java/feign/Target.java
new file mode 100644
index 0000000000..e2e02b31da
--- /dev/null
+++ b/core/src/main/java/feign/Target.java
@@ -0,0 +1,191 @@
+/**
+ * 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
+ * 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.
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..d2f30514c0
--- /dev/null
+++ b/core/src/main/java/feign/Types.java
@@ -0,0 +1,465 @@
+/**
+ * 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
+ * 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..74819d2fc9
--- /dev/null
+++ b/core/src/main/java/feign/Util.java
@@ -0,0 +1,326 @@
+/**
+ * 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
+ * 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..e032bc8d7b
--- /dev/null
+++ b/core/src/main/java/feign/auth/Base64.java
@@ -0,0 +1,158 @@
+/**
+ * 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
+ * the License.
+ */
+package feign.auth;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ * copied from okhttp
+ *
+ * @author Alexander Y. Kleymenov
+ */
+final class Base64 {
+
+ public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
+ private static final byte[] MAP = new byte[]{
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
+ 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',
+ 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4',
+ '5', '6', '7', '8', '9', '+', '/'
+ };
+
+ private Base64() {
+ }
+
+ public static byte[] decode(byte[] in) {
+ return decode(in, in.length);
+ }
+
+ public static byte[] decode(byte[] in, int len) {
+ // approximate output length
+ int length = len / 4 * 3;
+ // return an empty array on empty or short input without padding
+ if (length == 0) {
+ return EMPTY_BYTE_ARRAY;
+ }
+ // temporary array
+ byte[] out = new byte[length];
+ // number of padding characters ('=')
+ int pad = 0;
+ byte chr;
+ // compute the number of the padding characters
+ // and adjust the length of the input
+ for (; ; len--) {
+ chr = in[len - 1];
+ // skip the neutral characters
+ if ((chr == '\n') || (chr == '\r') || (chr == ' ') || (chr == '\t')) {
+ continue;
+ }
+ if (chr == '=') {
+ pad++;
+ } else {
+ break;
+ }
+ }
+ // index in the output array
+ int outIndex = 0;
+ // index in the input array
+ int inIndex = 0;
+ // holds the value of the input character
+ int bits = 0;
+ // holds the value of the input quantum
+ int quantum = 0;
+ for (int i = 0; i < len; i++) {
+ chr = in[i];
+ // skip the neutral characters
+ if ((chr == '\n') || (chr == '\r') || (chr == ' ') || (chr == '\t')) {
+ continue;
+ }
+ if ((chr >= 'A') && (chr <= 'Z')) {
+ // char ASCII value
+ // A 65 0
+ // Z 90 25 (ASCII - 65)
+ bits = chr - 65;
+ } else if ((chr >= 'a') && (chr <= 'z')) {
+ // char ASCII value
+ // a 97 26
+ // z 122 51 (ASCII - 71)
+ bits = chr - 71;
+ } else if ((chr >= '0') && (chr <= '9')) {
+ // char ASCII value
+ // 0 48 52
+ // 9 57 61 (ASCII + 4)
+ bits = chr + 4;
+ } else if (chr == '+') {
+ bits = 62;
+ } else if (chr == '/') {
+ bits = 63;
+ } else {
+ return null;
+ }
+ // append the value to the quantum
+ quantum = (quantum << 6) | (byte) bits;
+ if (inIndex % 4 == 3) {
+ // 4 characters were read, so make the output:
+ out[outIndex++] = (byte) (quantum >> 16);
+ out[outIndex++] = (byte) (quantum >> 8);
+ out[outIndex++] = (byte) quantum;
+ }
+ inIndex++;
+ }
+ if (pad > 0) {
+ // adjust the quantum value according to the padding
+ quantum = quantum << (6 * pad);
+ // make output
+ out[outIndex++] = (byte) (quantum >> 16);
+ if (pad == 1) {
+ out[outIndex++] = (byte) (quantum >> 8);
+ }
+ }
+ // create the resulting array
+ byte[] result = new byte[outIndex];
+ System.arraycopy(out, 0, result, 0, outIndex);
+ return result;
+ }
+
+ public static String encode(byte[] in) {
+ int length = (in.length + 2) * 4 / 3;
+ byte[] out = new byte[length];
+ int index = 0, end = in.length - in.length % 3;
+ for (int i = 0; i < end; i += 3) {
+ out[index++] = MAP[(in[i] & 0xff) >> 2];
+ out[index++] = MAP[((in[i] & 0x03) << 4) | ((in[i + 1] & 0xff) >> 4)];
+ out[index++] = MAP[((in[i + 1] & 0x0f) << 2) | ((in[i + 2] & 0xff) >> 6)];
+ out[index++] = MAP[(in[i + 2] & 0x3f)];
+ }
+ switch (in.length % 3) {
+ case 1:
+ out[index++] = MAP[(in[end] & 0xff) >> 2];
+ out[index++] = MAP[(in[end] & 0x03) << 4];
+ out[index++] = '=';
+ out[index++] = '=';
+ break;
+ case 2:
+ out[index++] = MAP[(in[end] & 0xff) >> 2];
+ out[index++] = MAP[((in[end] & 0x03) << 4) | ((in[end + 1] & 0xff) >> 4)];
+ out[index++] = MAP[((in[end + 1] & 0x0f) << 2)];
+ out[index++] = '=';
+ break;
+ }
+ try {
+ return new String(out, 0, index, "US-ASCII");
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError(e);
+ }
+ }
+}
+
diff --git a/core/src/main/java/feign/auth/BasicAuthRequestInterceptor.java b/core/src/main/java/feign/auth/BasicAuthRequestInterceptor.java
new file mode 100644
index 0000000000..202d048365
--- /dev/null
+++ b/core/src/main/java/feign/auth/BasicAuthRequestInterceptor.java
@@ -0,0 +1,69 @@
+/**
+ * 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
+ * the License.
+ */
+package feign.auth;
+
+import java.nio.charset.Charset;
+
+import feign.RequestInterceptor;
+import feign.RequestTemplate;
+
+import static feign.Util.ISO_8859_1;
+import static feign.Util.checkNotNull;
+
+/**
+ * An interceptor that adds the request header needed to use HTTP basic authentication.
+ */
+public class BasicAuthRequestInterceptor implements RequestInterceptor {
+
+ private final String headerValue;
+
+ /**
+ * Creates an interceptor that authenticates all requests with the specified username and password
+ * encoded using ISO-8859-1.
+ *
+ * @param username the username to use for authentication
+ * @param password the password to use for authentication
+ */
+ public BasicAuthRequestInterceptor(String username, String password) {
+ this(username, password, ISO_8859_1);
+ }
+
+ /**
+ * Creates an interceptor that authenticates all requests with the specified username and password
+ * encoded using the specified charset.
+ *
+ * @param username the username to use for authentication
+ * @param password the password to use for authentication
+ * @param charset the charset to use when encoding the credentials
+ */
+ public BasicAuthRequestInterceptor(String username, String password, Charset charset) {
+ checkNotNull(username, "username");
+ checkNotNull(password, "password");
+ this.headerValue = "Basic " + base64Encode((username + ":" + password).getBytes(charset));
+ }
+
+ /*
+ * This uses a Sun internal method; if we ever encounter a case where this method is not available, the appropriate
+ * response would be to pull the necessary portions of Guava's BaseEncoding class into Util.
+ */
+ private static String base64Encode(byte[] bytes) {
+ return Base64.encode(bytes);
+ }
+
+ @Override
+ public void apply(RequestTemplate template) {
+ template.header("Authorization", headerValue);
+ }
+}
+
diff --git a/core/src/main/java/feign/codec/DecodeException.java b/core/src/main/java/feign/codec/DecodeException.java
new file mode 100644
index 0000000000..2baeb84178
--- /dev/null
+++ b/core/src/main/java/feign/codec/DecodeException.java
@@ -0,0 +1,43 @@
+/**
+ * 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
+ * the License.
+ */
+package feign.codec;
+
+import feign.FeignException;
+
+import static feign.Util.checkNotNull;
+
+/**
+ * Similar to {@code javax.websocket.DecodeException}, raised when a problem occurs decoding a
+ * message. Note that {@code DecodeException} is not an {@code IOException}, nor does it have one
+ * set as its cause.
+ */
+public class DecodeException extends FeignException {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * @param message the reason for the failure.
+ */
+ public DecodeException(String message) {
+ super(checkNotNull(message, "message"));
+ }
+
+ /**
+ * @param message possibly null reason for the failure.
+ * @param cause the cause of the error.
+ */
+ public DecodeException(String message, Throwable cause) {
+ super(message, checkNotNull(cause, "cause"));
+ }
+}
diff --git a/core/src/main/java/feign/codec/Decoder.java b/core/src/main/java/feign/codec/Decoder.java
new file mode 100644
index 0000000000..b7d24a00fb
--- /dev/null
+++ b/core/src/main/java/feign/codec/Decoder.java
@@ -0,0 +1,85 @@
+/**
+ * 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
+ * the License.
+ */
+package feign.codec;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+
+import feign.Feign;
+import feign.FeignException;
+import feign.Response;
+import feign.Util;
+
+/**
+ * Decodes an HTTP response into a single object of the given {@code type}. Invoked when {@link
+ * Response#status()} is in the 2xx range and the return type is neither {@code void} nor {@code
+ * Response}. Example Implementation:
+ *
The {@code type} parameter will correspond to the {@link
+ * java.lang.reflect.Method#getGenericReturnType() generic return type} of an {@link
+ * feign.Target#type() interface} processed by {@link feign.Feign#newInstance(feign.Target)}. When
+ * writing your implementation of Decoder, ensure you also test parameterized types such as {@code
+ * List}.
+ *
Note on exception propagation
Exceptions thrown by {@link Decoder}s get wrapped in
+ * a {@link DecodeException} unless they are a subclass of {@link FeignException} already, and unless
+ * the client was configured with {@link Feign.Builder#decode404()}.
+ */
+public interface Decoder {
+
+ /**
+ * Decodes an http response into an object corresponding to its {@link
+ * java.lang.reflect.Method#getGenericReturnType() generic return type}. If you need to wrap
+ * exceptions, please do so via {@link DecodeException}.
+ *
+ * @param response the response to decode
+ * @param type {@link java.lang.reflect.Method#getGenericReturnType() generic return type} of
+ * the method corresponding to this {@code response}.
+ * @return instance of {@code type}
+ * @throws IOException will be propagated safely to the caller.
+ * @throws DecodeException when decoding failed due to a checked exception besides IOException.
+ * @throws FeignException when decoding succeeds, but conveys the operation failed.
+ */
+ Object decode(Response response, Type type) throws IOException, DecodeException, FeignException;
+
+ /** Default implementation of {@code Decoder}. */
+ public class Default extends StringDecoder {
+
+ @Override
+ public Object decode(Response response, Type type) throws IOException {
+ if (response.status() == 404) return Util.emptyValueOf(type);
+ if (response.body() == null) return null;
+ if (byte[].class.equals(type)) {
+ return Util.toByteArray(response.body().asInputStream());
+ }
+ return super.decode(response, type);
+ }
+ }
+}
diff --git a/core/src/main/java/feign/codec/EncodeException.java b/core/src/main/java/feign/codec/EncodeException.java
new file mode 100644
index 0000000000..9cb40cf074
--- /dev/null
+++ b/core/src/main/java/feign/codec/EncodeException.java
@@ -0,0 +1,43 @@
+/**
+ * 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
+ * the License.
+ */
+package feign.codec;
+
+import feign.FeignException;
+
+import static feign.Util.checkNotNull;
+
+/**
+ * Similar to {@code javax.websocket.EncodeException}, raised when a problem occurs encoding a
+ * message. Note that {@code EncodeException} is not an {@code IOException}, nor does it have one
+ * set as its cause.
+ */
+public class EncodeException extends FeignException {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * @param message the reason for the failure.
+ */
+ public EncodeException(String message) {
+ super(checkNotNull(message, "message"));
+ }
+
+ /**
+ * @param message possibly null reason for the failure.
+ * @param cause the cause of the error.
+ */
+ public EncodeException(String message, Throwable cause) {
+ super(message, checkNotNull(cause, "cause"));
+ }
+}
diff --git a/core/src/main/java/feign/codec/Encoder.java b/core/src/main/java/feign/codec/Encoder.java
new file mode 100644
index 0000000000..7d5a43f3b6
--- /dev/null
+++ b/core/src/main/java/feign/codec/Encoder.java
@@ -0,0 +1,92 @@
+/**
+ * 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
+ * the License.
+ */
+package feign.codec;
+
+import java.lang.reflect.Type;
+
+import feign.RequestTemplate;
+import feign.Util;
+
+import static java.lang.String.format;
+
+/**
+ * Encodes an object into an HTTP request body. Like {@code javax.websocket.Encoder}. {@code
+ * Encoder} is used when a method parameter has no {@code @Param} annotation. For example:
+ *
+ *
+ * public class GsonEncoder implements Encoder {
+ * private final Gson gson;
+ *
+ * public GsonEncoder(Gson gson) {
+ * this.gson = gson;
+ * }
+ *
+ * @Override
+ * public void encode(Object object, Type bodyType, RequestTemplate template) {
+ * template.body(gson.toJson(object, bodyType));
+ * }
+ * }
+ *
+ *
+ *
Form encoding
If any parameters are found in {@link
+ * feign.MethodMetadata#formParams()}, they will be collected and passed to the Encoder as a map.
+ *
+ *
Ex. The following is a form. Notice the parameters aren't consumed in the request line. A map
+ * including "username" and "password" keys will passed to the encoder, and the body type will be
+ * {@link #MAP_STRING_WILDCARD}.
+ *
+ */
+public interface Encoder {
+ /** Type literal for {@code Map}, indicating the object to encode is a form. */
+ Type MAP_STRING_WILDCARD = Util.MAP_STRING_WILDCARD;
+
+ /**
+ * Converts objects to an appropriate representation in the template.
+ *
+ * @param object what to encode as the request body.
+ * @param bodyType the type the object should be encoded as. {@link #MAP_STRING_WILDCARD}
+ * indicates form encoding.
+ * @param template the request template to populate.
+ * @throws EncodeException when encoding failed due to a checked exception.
+ */
+ void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException;
+
+ /**
+ * Default implementation of {@code Encoder}.
+ */
+ class Default implements Encoder {
+
+ @Override
+ public void encode(Object object, Type bodyType, RequestTemplate template) {
+ if (bodyType == String.class) {
+ template.body(object.toString());
+ } else if (bodyType == byte[].class) {
+ template.body((byte[]) object, null);
+ } else if (object != null) {
+ throw new EncodeException(
+ format("%s is not a type supported by this encoder.", object.getClass()));
+ }
+ }
+ }
+}
diff --git a/core/src/main/java/feign/codec/ErrorDecoder.java b/core/src/main/java/feign/codec/ErrorDecoder.java
new file mode 100644
index 0000000000..4a52226c5b
--- /dev/null
+++ b/core/src/main/java/feign/codec/ErrorDecoder.java
@@ -0,0 +1,151 @@
+/**
+ * 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
+ * the License.
+ */
+package feign.codec;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Map;
+
+import feign.FeignException;
+import feign.Response;
+import feign.RetryableException;
+
+import static feign.FeignException.errorStatus;
+import static feign.Util.RETRY_AFTER;
+import static feign.Util.checkNotNull;
+import static java.util.Locale.US;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+/**
+ * Allows you to massage an exception into a application-specific one. Converting out to a throttle
+ * exception are examples of this in use.
+ *
+ * Ex:
+ *
+ * class IllegalArgumentExceptionOn404Decoder implements ErrorDecoder {
+ *
+ * @Override
+ * public Exception decode(String methodKey, Response response) {
+ * if (response.status() == 400)
+ * throw new IllegalArgumentException("bad zone name");
+ * return new ErrorDecoder.Default().decode(methodKey, response);
+ * }
+ *
+ * }
+ *
+ *
+ * Error handling
+ *
+ * Responses where {@link Response#status()} is not in the 2xx
+ * range are classified as errors, addressed by the {@link ErrorDecoder}. That said, certain RPC
+ * apis return errors defined in the {@link Response#body()} even on a 200 status. For example, in
+ * the DynECT api, a job still running condition is returned with a 200 status, encoded in json.
+ * When scenarios like this occur, you should raise an application-specific exception (which may be
+ * {@link feign.RetryableException retryable}).
+ *
+ * Not Found Semantics
+ * It is commonly the case that 404 (Not Found) status has semantic value in HTTP apis. While
+ * the default behavior is to raise exeception, users can alternatively enable 404 processing via
+ * {@link feign.Feign.Builder#decode404()}.
+ */
+public interface ErrorDecoder {
+
+ /**
+ * Implement this method in order to decode an HTTP {@link Response} when {@link
+ * Response#status()} is not in the 2xx range. Please raise application-specific exceptions where
+ * possible. If your exception is retryable, wrap or subclass {@link RetryableException}
+ *
+ * @param methodKey {@link feign.Feign#configKey} of the java method that invoked the request.
+ * ex. {@code IAM#getUser()}
+ * @param response HTTP response where {@link Response#status() status} is greater than or equal
+ * to {@code 300}.
+ * @return Exception IOException, if there was a network error reading the response or an
+ * application-specific exception decoded by the implementation. If the throwable is retryable, it
+ * should be wrapped, or a subtype of {@link RetryableException}
+ */
+ public Exception decode(String methodKey, Response response);
+
+ public static class Default implements ErrorDecoder {
+
+ private final RetryAfterDecoder retryAfterDecoder = new RetryAfterDecoder();
+
+ @Override
+ public Exception decode(String methodKey, Response response) {
+ FeignException exception = errorStatus(methodKey, response);
+ Date retryAfter = retryAfterDecoder.apply(firstOrNull(response.headers(), RETRY_AFTER));
+ if (retryAfter != null) {
+ return new RetryableException(exception.getMessage(), exception, retryAfter);
+ }
+ return exception;
+ }
+
+ private T firstOrNull(Map> map, String key) {
+ if (map.containsKey(key) && !map.get(key).isEmpty()) {
+ return map.get(key).iterator().next();
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Decodes a {@link feign.Util#RETRY_AFTER} header into an absolute date, if possible. See Retry-After format
+ */
+ static class RetryAfterDecoder {
+
+ static final DateFormat
+ RFC822_FORMAT =
+ new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", US);
+ private final DateFormat rfc822Format;
+
+ RetryAfterDecoder() {
+ this(RFC822_FORMAT);
+ }
+
+ RetryAfterDecoder(DateFormat rfc822Format) {
+ this.rfc822Format = checkNotNull(rfc822Format, "rfc822Format");
+ }
+
+ protected long currentTimeMillis() {
+ return System.currentTimeMillis();
+ }
+
+ /**
+ * returns a date that corresponds to the first time a request can be retried.
+ *
+ * @param retryAfter String in Retry-After format
+ */
+ public Date apply(String retryAfter) {
+ if (retryAfter == null) {
+ return null;
+ }
+ if (retryAfter.matches("^[0-9]+$")) {
+ long deltaMillis = SECONDS.toMillis(Long.parseLong(retryAfter));
+ return new Date(currentTimeMillis() + deltaMillis);
+ }
+ synchronized (rfc822Format) {
+ try {
+ return rfc822Format.parse(retryAfter);
+ } catch (ParseException ignored) {
+ return null;
+ }
+ }
+ }
+ }
+}
diff --git a/core/src/main/java/feign/codec/StringDecoder.java b/core/src/main/java/feign/codec/StringDecoder.java
new file mode 100644
index 0000000000..194f8f3d74
--- /dev/null
+++ b/core/src/main/java/feign/codec/StringDecoder.java
@@ -0,0 +1,37 @@
+/**
+ * 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
+ * the License.
+ */
+package feign.codec;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+
+import feign.Response;
+import feign.Util;
+
+import static java.lang.String.format;
+
+public class StringDecoder implements Decoder {
+
+ @Override
+ public Object decode(Response response, Type type) throws IOException {
+ Response.Body body = response.body();
+ if (body == null) {
+ return null;
+ }
+ if (String.class.equals(type)) {
+ return Util.toString(body.asReader());
+ }
+ throw new DecodeException(format("%s is not a type supported by this decoder.", type));
+ }
+}
diff --git a/core/src/test/java/feign/BaseApiTest.java b/core/src/test/java/feign/BaseApiTest.java
new file mode 100644
index 0000000000..b1ec0e54b3
--- /dev/null
+++ b/core/src/test/java/feign/BaseApiTest.java
@@ -0,0 +1,113 @@
+/**
+ * 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
+ * the License.
+ */
+package feign;
+
+import com.google.gson.reflect.TypeToken;
+
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.lang.reflect.Type;
+import java.util.List;
+
+import feign.codec.Decoder;
+import feign.codec.Encoder;
+
+import static feign.assertj.MockWebServerAssertions.assertThat;
+
+public class BaseApiTest {
+
+ @Rule
+ public final MockWebServer server = new MockWebServer();
+
+ interface BaseApi {
+
+ @RequestLine("GET /api/{key}")
+ Entity get(@Param("key") K key);
+
+ @RequestLine("POST /api")
+ Entities getAll(Keys keys);
+ }
+
+ static class Keys {
+
+ List keys;
+ }
+
+ static class Entity {
+
+ K key;
+ M model;
+ }
+
+ static class Entities {
+
+ List> entities;
+ }
+
+ interface MyApi extends BaseApi {
+
+ }
+
+ @Test
+ public void resolvesParameterizedResult() throws InterruptedException {
+ server.enqueue(new MockResponse().setBody("foo"));
+
+ String baseUrl = server.url("/default").toString();
+
+ Feign.builder()
+ .decoder(new Decoder() {
+ @Override
+ public Object decode(Response response, Type type) {
+ assertThat(type)
+ .isEqualTo(new TypeToken>() {
+ }.getType());
+ return null;
+ }
+ })
+ .target(MyApi.class, baseUrl).get("foo");
+
+ assertThat(server.takeRequest()).hasPath("/default/api/foo");
+ }
+
+ @Test
+ public void resolvesBodyParameter() throws InterruptedException {
+ server.enqueue(new MockResponse().setBody("foo"));
+
+ String baseUrl = server.url("/default").toString();
+
+ Feign.builder()
+ .encoder(new Encoder() {
+ @Override
+ public void encode(Object object, Type bodyType, RequestTemplate template) {
+ assertThat(bodyType)
+ .isEqualTo(new TypeToken>() {
+ }.getType());
+ }
+ })
+ .decoder(new Decoder() {
+ @Override
+ public Object decode(Response response, Type type) {
+ assertThat(type)
+ .isEqualTo(new TypeToken>() {
+ }.getType());
+ return null;
+ }
+ })
+ .target(MyApi.class, baseUrl).getAll(new Keys());
+ }
+}
diff --git a/core/src/test/java/feign/ContractWithRuntimeInjectionTest.java b/core/src/test/java/feign/ContractWithRuntimeInjectionTest.java
new file mode 100644
index 0000000000..23342a9081
--- /dev/null
+++ b/core/src/test/java/feign/ContractWithRuntimeInjectionTest.java
@@ -0,0 +1,125 @@
+/**
+ * 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
+ * the License.
+ */
+package feign;
+
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import static feign.assertj.MockWebServerAssertions.assertThat;
+
+public class ContractWithRuntimeInjectionTest {
+
+ static class CaseExpander implements Param.Expander {
+
+ private final boolean lowercase;
+
+ CaseExpander() {
+ this(false);
+ }
+
+ CaseExpander(boolean lowercase) {
+ this.lowercase = lowercase;
+ }
+
+
+ @Override
+ public String expand(Object value) {
+ return lowercase ? value.toString().toLowerCase() : value.toString();
+ }
+ }
+
+ @Rule
+ public final MockWebServer server = new MockWebServer();
+
+ interface TestExpander {
+
+ @RequestLine("GET /path?query={query}")
+ Response get(@Param(value = "query", expander = CaseExpander.class) String query);
+ }
+
+ @Test
+ public void baseCaseExpanderNewInstance() throws InterruptedException {
+ server.enqueue(new MockResponse());
+
+ String baseUrl = server.url("/default").toString();
+
+ Feign.builder().target(TestExpander.class, baseUrl).get("FOO");
+
+ assertThat(server.takeRequest()).hasPath("/default/path?query=FOO");
+ }
+
+ @Configuration
+ static class FeignConfiguration {
+
+ @Bean
+ CaseExpander lowercaseExpander() {
+ return new CaseExpander(true);
+ }
+
+ @Bean
+ Contract contract(BeanFactory beanFactory) {
+ return new ContractWithRuntimeInjection(beanFactory);
+ }
+ }
+
+ static class ContractWithRuntimeInjection extends Contract.Default {
+ final BeanFactory beanFactory;
+
+ ContractWithRuntimeInjection(BeanFactory beanFactory) {
+ this.beanFactory = beanFactory;
+ }
+
+ /**
+ * Injects {@link MethodMetadata#indexToExpander(Map)} via {@link BeanFactory#getBean(Class)}.
+ */
+ @Override
+ public List parseAndValidatateMetadata(Class> targetType) {
+ List result = super.parseAndValidatateMetadata(targetType);
+ for (MethodMetadata md : result) {
+ Map indexToExpander = new LinkedHashMap();
+ for (Map.Entry> entry : md.indexToExpanderClass().entrySet()) {
+ indexToExpander.put(entry.getKey(), beanFactory.getBean(entry.getValue()));
+ }
+ md.indexToExpander(indexToExpander);
+ }
+ return result;
+ }
+ }
+
+ @Test
+ public void contractWithRuntimeInjection() throws InterruptedException {
+ server.enqueue(new MockResponse());
+
+ String baseUrl = server.url("/default").toString();
+ ApplicationContext context = new AnnotationConfigApplicationContext(FeignConfiguration.class);
+
+ Feign.builder()
+ .contract(context.getBean(Contract.class))
+ .target(TestExpander.class, baseUrl).get("FOO");
+
+ assertThat(server.takeRequest()).hasPath("/default/path?query=foo");
+ }
+}
diff --git a/core/src/test/java/feign/CustomPojo.java b/core/src/test/java/feign/CustomPojo.java
new file mode 100644
index 0000000000..8a162afbeb
--- /dev/null
+++ b/core/src/test/java/feign/CustomPojo.java
@@ -0,0 +1,25 @@
+/**
+ * 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
+ * the License.
+ */
+package feign;
+
+public class CustomPojo {
+
+ private final String name;
+ private final Integer number;
+
+ CustomPojo(String name, Integer number) {
+ this.name = name;
+ this.number = number;
+ }
+}
diff --git a/core/src/test/java/feign/DefaultContractTest.java b/core/src/test/java/feign/DefaultContractTest.java
new file mode 100644
index 0000000000..556c9edfe6
--- /dev/null
+++ b/core/src/test/java/feign/DefaultContractTest.java
@@ -0,0 +1,832 @@
+/**
+ * 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
+ * the License.
+ */
+package feign;
+
+import com.google.gson.reflect.TypeToken;
+
+import org.assertj.core.api.Fail;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.net.URI;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedMap;
+
+import static feign.assertj.FeignAssertions.assertThat;
+import static java.util.Arrays.asList;
+import static org.assertj.core.data.MapEntry.entry;
+
+/**
+ * Tests interfaces defined per {@link Contract.Default} are interpreted into expected {@link feign
+ * .RequestTemplate template} instances.
+ */
+public class DefaultContractTest {
+
+ @Rule
+ public final ExpectedException thrown = ExpectedException.none();
+
+ Contract.Default contract = new Contract.Default();
+
+ @Test
+ public void httpMethods() throws Exception {
+ assertThat(parseAndValidateMetadata(Methods.class, "post").template())
+ .hasMethod("POST");
+
+ assertThat(parseAndValidateMetadata(Methods.class, "put").template())
+ .hasMethod("PUT");
+
+ assertThat(parseAndValidateMetadata(Methods.class, "get").template())
+ .hasMethod("GET");
+
+ assertThat(parseAndValidateMetadata(Methods.class, "delete").template())
+ .hasMethod("DELETE");
+ }
+
+ @Test
+ public void bodyParamIsGeneric() throws Exception {
+ MethodMetadata md = parseAndValidateMetadata(BodyParams.class, "post", List.class);
+
+ assertThat(md.bodyIndex())
+ .isEqualTo(0);
+ assertThat(md.bodyType())
+ .isEqualTo(new TypeToken>() {
+ }.getType());
+ }
+
+ @Test
+ public void bodyParamWithPathParam() throws Exception {
+ MethodMetadata md = parseAndValidateMetadata(BodyParams.class, "post", int.class, List.class);
+
+ assertThat(md.bodyIndex())
+ .isEqualTo(1);
+ assertThat(md.indexToName()).containsOnly(
+ entry(0, asList("id"))
+ );
+ }
+
+ @Test
+ public void tooManyBodies() throws Exception {
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("Method has too many Body");
+ parseAndValidateMetadata(BodyParams.class, "tooMany", List.class, List.class);
+ }
+
+ @Test
+ public void customMethodWithoutPath() throws Exception {
+ assertThat(parseAndValidateMetadata(CustomMethod.class, "patch").template())
+ .hasMethod("PATCH")
+ .hasUrl("");
+ }
+
+ @Test
+ public void queryParamsInPathExtract() throws Exception {
+ assertThat(parseAndValidateMetadata(WithQueryParamsInPath.class, "none").template())
+ .hasUrl("/")
+ .hasQueries();
+
+ assertThat(parseAndValidateMetadata(WithQueryParamsInPath.class, "one").template())
+ .hasUrl("/")
+ .hasQueries(
+ entry("Action", asList("GetUser"))
+ );
+
+ assertThat(parseAndValidateMetadata(WithQueryParamsInPath.class, "two").template())
+ .hasUrl("/")
+ .hasQueries(
+ entry("Action", asList("GetUser")),
+ entry("Version", asList("2010-05-08"))
+ );
+
+ assertThat(parseAndValidateMetadata(WithQueryParamsInPath.class, "three").template())
+ .hasUrl("/")
+ .hasQueries(
+ entry("Action", asList("GetUser")),
+ entry("Version", asList("2010-05-08")),
+ entry("limit", asList("1"))
+ );
+
+ assertThat(parseAndValidateMetadata(WithQueryParamsInPath.class, "twoAndOneEmpty").template())
+ .hasUrl("/")
+ .hasQueries(
+ entry("flag", asList(new String[]{null})),
+ entry("Action", asList("GetUser")),
+ entry("Version", asList("2010-05-08"))
+ );
+
+ assertThat(parseAndValidateMetadata(WithQueryParamsInPath.class, "oneEmpty").template())
+ .hasUrl("/")
+ .hasQueries(
+ entry("flag", asList(new String[]{null}))
+ );
+
+ assertThat(parseAndValidateMetadata(WithQueryParamsInPath.class, "twoEmpty").template())
+ .hasUrl("/")
+ .hasQueries(
+ entry("flag", asList(new String[]{null})),
+ entry("NoErrors", asList(new String[]{null}))
+ );
+ }
+
+ @Test
+ public void bodyWithoutParameters() throws Exception {
+ MethodMetadata md = parseAndValidateMetadata(BodyWithoutParameters.class, "post");
+
+ assertThat(md.template())
+ .hasBody("");
+ }
+
+ @Test
+ public void headersOnMethodAddsContentTypeHeader() throws Exception {
+ MethodMetadata md = parseAndValidateMetadata(BodyWithoutParameters.class, "post");
+
+ assertThat(md.template())
+ .hasHeaders(
+ entry("Content-Type", asList("application/xml")),
+ entry("Content-Length", asList(String.valueOf(md.template().body().length)))
+ );
+ }
+
+ @Test
+ public void headersOnTypeAddsContentTypeHeader() throws Exception {
+ MethodMetadata md = parseAndValidateMetadata(HeadersOnType.class, "post");
+
+ assertThat(md.template())
+ .hasHeaders(
+ entry("Content-Type", asList("application/xml")),
+ entry("Content-Length", asList(String.valueOf(md.template().body().length)))
+ );
+ }
+
+ @Test
+ public void withPathAndURIParam() throws Exception {
+ MethodMetadata md = parseAndValidateMetadata(WithURIParam.class,
+ "uriParam", String.class, URI.class, String.class);
+
+ assertThat(md.indexToName())
+ .containsExactly(
+ entry(0, asList("1")),
+ // Skips 1 as it is a url index!
+ entry(2, asList("2"))
+ );
+
+ assertThat(md.urlIndex()).isEqualTo(1);
+ }
+
+ @Test
+ public void pathAndQueryParams() throws Exception {
+ MethodMetadata md = parseAndValidateMetadata(WithPathAndQueryParams.class,
+ "recordsByNameAndType", int.class, String.class,
+ String.class);
+
+ assertThat(md.template())
+ .hasQueries(entry("name", asList("{name}")), entry("type", asList("{type}")));
+
+ assertThat(md.indexToName()).containsExactly(
+ entry(0, asList("domainId")),
+ entry(1, asList("name")),
+ entry(2, asList("type"))
+ );
+ }
+
+ @Test
+ public void bodyWithTemplate() throws Exception {
+ MethodMetadata md = parseAndValidateMetadata(FormParams.class,
+ "login", String.class, String.class, String.class);
+
+ assertThat(md.template())
+ .hasBodyTemplate(
+ "%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D");
+ }
+
+ @Test
+ public void formParamsParseIntoIndexToName() throws Exception {
+ MethodMetadata md = parseAndValidateMetadata(FormParams.class,
+ "login", String.class, String.class, String.class);
+
+ assertThat(md.formParams())
+ .containsExactly("customer_name", "user_name", "password");
+
+ assertThat(md.indexToName()).containsExactly(
+ entry(0, asList("customer_name")),
+ entry(1, asList("user_name")),
+ entry(2, asList("password"))
+ );
+ }
+
+ /**
+ * Body type is only for the body param.
+ */
+ @Test
+ public void formParamsDoesNotSetBodyType() throws Exception {
+ MethodMetadata md = parseAndValidateMetadata(FormParams.class,
+ "login", String.class, String.class, String.class);
+
+ assertThat(md.bodyType()).isNull();
+ }
+
+ @Test
+ public void headerParamsParseIntoIndexToName() throws Exception {
+ MethodMetadata md = parseAndValidateMetadata(HeaderParams.class, "logout", String.class);
+
+ assertThat(md.template())
+ .hasHeaders(entry("Auth-Token", asList("{authToken}", "Foo")));
+
+ assertThat(md.indexToName())
+ .containsExactly(entry(0, asList("authToken")));
+ assertThat(md.formParams()).isEmpty();
+ }
+
+ @Test
+ public void headerParamsParseIntoIndexToNameNotAtStart() throws Exception {
+ MethodMetadata md = parseAndValidateMetadata(HeaderParamsNotAtStart.class, "logout", String.class);
+
+ assertThat(md.template())
+ .hasHeaders(entry("Authorization", asList("Bearer {authToken}", "Foo")));
+
+ assertThat(md.indexToName())
+ .containsExactly(entry(0, asList("authToken")));
+ assertThat(md.formParams()).isEmpty();
+ }
+
+ @Test
+ public void customExpander() throws Exception {
+ MethodMetadata md = parseAndValidateMetadata(CustomExpander.class, "date", Date.class);
+
+ assertThat(md.indexToExpanderClass())
+ .containsExactly(entry(0, DateToMillis.class));
+ }
+
+ @Test
+ public void queryMap() throws Exception {
+ MethodMetadata md = parseAndValidateMetadata(QueryMapTestInterface.class, "queryMap", Map.class);
+
+ assertThat(md.queryMapIndex()).isEqualTo(0);
+ }
+
+ @Test
+ public void queryMapEncodedDefault() throws Exception {
+ MethodMetadata md = parseAndValidateMetadata(QueryMapTestInterface.class, "queryMap", Map.class);
+
+ assertThat(md.queryMapEncoded()).isFalse();
+ }
+
+ @Test
+ public void queryMapEncodedTrue() throws Exception {
+ MethodMetadata md = parseAndValidateMetadata(QueryMapTestInterface.class, "queryMapEncoded", Map.class);
+
+ assertThat(md.queryMapEncoded()).isTrue();
+ }
+
+ @Test
+ public void queryMapEncodedFalse() throws Exception {
+ MethodMetadata md = parseAndValidateMetadata(QueryMapTestInterface.class, "queryMapNotEncoded", Map.class);
+
+ assertThat(md.queryMapEncoded()).isFalse();
+ }
+
+ @Test
+ public void queryMapMapSubclass() throws Exception {
+ MethodMetadata md = parseAndValidateMetadata(QueryMapTestInterface.class, "queryMapMapSubclass", SortedMap.class);
+
+ assertThat(md.queryMapIndex()).isEqualTo(0);
+ }
+
+ @Test
+ public void onlyOneQueryMapAnnotationPermitted() throws Exception {
+ try {
+ parseAndValidateMetadata(QueryMapTestInterface.class, "multipleQueryMap", Map.class, Map.class);
+ Fail.failBecauseExceptionWasNotThrown(IllegalStateException.class);
+ } catch (IllegalStateException ex) {
+ assertThat(ex).hasMessage("QueryMap annotation was present on multiple parameters.");
+ }
+ }
+
+ @Test
+ public void queryMapKeysMustBeStrings() throws Exception {
+ try {
+ parseAndValidateMetadata(QueryMapTestInterface.class, "nonStringKeyQueryMap", Map.class);
+ Fail.failBecauseExceptionWasNotThrown(IllegalStateException.class);
+ } catch (IllegalStateException ex) {
+ assertThat(ex).hasMessage("QueryMap key must be a String: Integer");
+ }
+ }
+
+ @Test
+ public void queryMapPojoObject() throws Exception {
+ MethodMetadata md = parseAndValidateMetadata(QueryMapTestInterface.class, "pojoObject", Object.class);
+
+ assertThat(md.queryMapIndex()).isEqualTo(0);
+ }
+
+ @Test
+ public void queryMapPojoObjectEncoded() throws Exception {
+ MethodMetadata md = parseAndValidateMetadata(QueryMapTestInterface.class, "pojoObjectEncoded", Object.class);
+
+ assertThat(md.queryMapIndex()).isEqualTo(0);
+ assertThat(md.queryMapEncoded()).isTrue();
+ }
+
+ @Test
+ public void queryMapPojoObjectNotEncoded() throws Exception {
+ MethodMetadata md = parseAndValidateMetadata(QueryMapTestInterface.class, "pojoObjectNotEncoded", Object.class);
+
+ assertThat(md.queryMapIndex()).isEqualTo(0);
+ assertThat(md.queryMapEncoded()).isFalse();
+ }
+
+ @Test
+ public void slashAreEncodedWhenNeeded() throws Exception {
+ MethodMetadata md = parseAndValidateMetadata(SlashNeedToBeEncoded.class,
+ "getQueues", String.class);
+
+ assertThat(md.template().decodeSlash()).isFalse();
+
+ md = parseAndValidateMetadata(SlashNeedToBeEncoded.class, "getZone", String.class);
+
+ assertThat(md.template().decodeSlash()).isTrue();
+ }
+
+ @Test
+ public void onlyOneHeaderMapAnnotationPermitted() throws Exception {
+ try {
+ parseAndValidateMetadata(HeaderMapInterface.class, "multipleHeaderMap", Map.class, Map.class);
+ Fail.failBecauseExceptionWasNotThrown(IllegalStateException.class);
+ } catch (IllegalStateException ex) {
+ assertThat(ex).hasMessage("HeaderMap annotation was present on multiple parameters.");
+ }
+ }
+
+ interface Methods {
+
+ @RequestLine("POST /")
+ void post();
+
+ @RequestLine("PUT /")
+ void put();
+
+ @RequestLine("GET /")
+ void get();
+
+ @RequestLine("DELETE /")
+ void delete();
+ }
+
+ interface BodyParams {
+
+ @RequestLine("POST")
+ Response post(List body);
+
+ @RequestLine("PUT /offers/{id}")
+ void post(@Param("id") int id, List body);
+
+ @RequestLine("POST")
+ Response tooMany(List body, List body2);
+ }
+
+ interface CustomMethod {
+
+ @RequestLine("PATCH")
+ Response patch();
+ }
+
+ interface WithQueryParamsInPath {
+
+ @RequestLine("GET /")
+ Response none();
+
+ @RequestLine("GET /?Action=GetUser")
+ Response one();
+
+ @RequestLine("GET /?Action=GetUser&Version=2010-05-08")
+ Response two();
+
+ @RequestLine("GET /?Action=GetUser&Version=2010-05-08&limit=1")
+ Response three();
+
+ @RequestLine("GET /?flag&Action=GetUser&Version=2010-05-08")
+ Response twoAndOneEmpty();
+
+ @RequestLine("GET /?flag")
+ Response oneEmpty();
+
+ @RequestLine("GET /?flag&NoErrors")
+ Response twoEmpty();
+ }
+
+ interface BodyWithoutParameters {
+
+ @RequestLine("POST /")
+ @Headers("Content-Type: application/xml")
+ @Body("")
+ Response post();
+ }
+
+ @Headers("Content-Type: application/xml")
+ interface HeadersOnType {
+
+ @RequestLine("POST /")
+ @Body("")
+ Response post();
+ }
+
+ interface WithURIParam {
+
+ @RequestLine("GET /{1}/{2}")
+ Response uriParam(@Param("1") String one, URI endpoint, @Param("2") String two);
+ }
+
+ interface WithPathAndQueryParams {
+
+ @RequestLine("GET /domains/{domainId}/records?name={name}&type={type}")
+ Response recordsByNameAndType(@Param("domainId") int id, @Param("name") String nameFilter,
+ @Param("type") String typeFilter);
+ }
+
+ interface FormParams {
+
+ @RequestLine("POST /")
+ @Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D")
+ void login(
+ @Param("customer_name") String customer,
+ @Param("user_name") String user, @Param("password") String password);
+ }
+
+ interface HeaderMapInterface {
+
+ @RequestLine("POST /")
+ void multipleHeaderMap(@HeaderMap Map headers, @HeaderMap Map queries);
+ }
+
+ interface HeaderParams {
+
+ @RequestLine("POST /")
+ @Headers({"Auth-Token: {authToken}", "Auth-Token: Foo"})
+ void logout(@Param("authToken") String token);
+ }
+
+ interface HeaderParamsNotAtStart {
+
+ @RequestLine("POST /")
+ @Headers({"Authorization: Bearer {authToken}", "Authorization: Foo"})
+ void logout(@Param("authToken") String token);
+ }
+
+ interface CustomExpander {
+
+ @RequestLine("POST /?date={date}")
+ void date(@Param(value = "date", expander = DateToMillis.class) Date date);
+ }
+
+ class DateToMillis implements Param.Expander {
+
+ @Override
+ public String expand(Object value) {
+ return String.valueOf(((Date) value).getTime());
+ }
+ }
+
+ interface QueryMapTestInterface {
+
+ @RequestLine("POST /")
+ void queryMap(@QueryMap Map queryMap);
+
+ @RequestLine("POST /")
+ void queryMapMapSubclass(@QueryMap SortedMap queryMap);
+
+ @RequestLine("POST /")
+ void queryMapEncoded(@QueryMap(encoded = true) Map queryMap);
+
+ @RequestLine("POST /")
+ void queryMapNotEncoded(@QueryMap(encoded = false) Map queryMap);
+
+ @RequestLine("POST /")
+ void pojoObject(@QueryMap Object object);
+
+ @RequestLine("POST /")
+ void pojoObjectEncoded(@QueryMap(encoded = true) Object object);
+
+ @RequestLine("POST /")
+ void pojoObjectNotEncoded(@QueryMap(encoded = false) Object object);
+
+ // invalid
+ @RequestLine("POST /")
+ void multipleQueryMap(@QueryMap Map mapOne, @QueryMap Map mapTwo);
+
+ // invalid
+ @RequestLine("POST /")
+ void nonStringKeyQueryMap(@QueryMap Map queryMap);
+ }
+
+ interface SlashNeedToBeEncoded {
+ @RequestLine(value = "GET /api/queues/{vhost}", decodeSlash = false)
+ String getQueues(@Param("vhost") String vhost);
+
+ @RequestLine("GET /api/{zoneId}")
+ String getZone(@Param("ZoneId") String vhost);
+ }
+
+ @Headers("Foo: Bar")
+ interface SimpleParameterizedBaseApi {
+
+ @RequestLine("GET /api/{zoneId}")
+ M get(@Param("key") String key);
+ }
+
+ interface SimpleParameterizedApi extends SimpleParameterizedBaseApi {
+
+ }
+
+ @Test
+ public void simpleParameterizedBaseApi() throws Exception {
+ List md = contract.parseAndValidatateMetadata(SimpleParameterizedApi.class);
+
+ assertThat(md).hasSize(1);
+
+ assertThat(md.get(0).configKey())
+ .isEqualTo("SimpleParameterizedApi#get(String)");
+ assertThat(md.get(0).returnType())
+ .isEqualTo(String.class);
+ assertThat(md.get(0).template())
+ .hasHeaders(entry("Foo", asList("Bar")));
+ }
+
+ @Test
+ public void parameterizedApiUnsupported() throws Exception {
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("Parameterized types unsupported: SimpleParameterizedBaseApi");
+ contract.parseAndValidatateMetadata(SimpleParameterizedBaseApi.class);
+ }
+
+ interface OverrideParameterizedApi extends SimpleParameterizedBaseApi {
+
+ @Override
+ @RequestLine("GET /api/{zoneId}")
+ String get(@Param("key") String key);
+ }
+
+ @Test
+ public void overrideBaseApiUnsupported() throws Exception {
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("Overrides unsupported: OverrideParameterizedApi#get(String)");
+ contract.parseAndValidatateMetadata(OverrideParameterizedApi.class);
+ }
+
+ interface Child extends SimpleParameterizedBaseApi> {
+
+ }
+
+ interface GrandChild extends Child {
+
+ }
+
+ @Test
+ public void onlySingleLevelInheritanceSupported() throws Exception {
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("Only single-level inheritance supported: GrandChild");
+ contract.parseAndValidatateMetadata(GrandChild.class);
+ }
+
+ @Headers("Foo: Bar")
+ interface ParameterizedBaseApi {
+
+ @RequestLine("GET /api/{key}")
+ Entity get(@Param("key") K key);
+
+ @RequestLine("POST /api")
+ Entities getAll(Keys keys);
+ }
+
+ static class Keys {
+
+ List keys;
+ }
+
+ static class Entity {
+
+ K key;
+ M model;
+ }
+
+ static class Entities {
+
+ private List> entities;
+ }
+
+ @Headers("Version: 1")
+ interface ParameterizedApi extends ParameterizedBaseApi {
+
+ }
+
+ @Test
+ public void parameterizedBaseApi() throws Exception {
+ List md = contract.parseAndValidatateMetadata(ParameterizedApi.class);
+
+ Map byConfigKey = new LinkedHashMap();
+ for (MethodMetadata m : md) {
+ byConfigKey.put(m.configKey(), m);
+ }
+
+ assertThat(byConfigKey)
+ .containsOnlyKeys("ParameterizedApi#get(String)", "ParameterizedApi#getAll(Keys)");
+
+ assertThat(byConfigKey.get("ParameterizedApi#get(String)").returnType())
+ .isEqualTo(new TypeToken>() {
+ }.getType());
+ assertThat(byConfigKey.get("ParameterizedApi#get(String)").template()).hasHeaders(
+ entry("Version", asList("1")),
+ entry("Foo", asList("Bar"))
+ );
+
+ assertThat(byConfigKey.get("ParameterizedApi#getAll(Keys)").returnType())
+ .isEqualTo(new TypeToken>() {
+ }.getType());
+ assertThat(byConfigKey.get("ParameterizedApi#getAll(Keys)").bodyType())
+ .isEqualTo(new TypeToken>() {
+ }.getType());
+ assertThat(byConfigKey.get("ParameterizedApi#getAll(Keys)").template()).hasHeaders(
+ entry("Version", asList("1")),
+ entry("Foo", asList("Bar"))
+ );
+ }
+
+ @Headers("Authorization: {authHdr}")
+ interface ParameterizedHeaderExpandApi {
+ @RequestLine("GET /api/{zoneId}")
+ @Headers("Accept: application/json")
+ String getZone(@Param("zoneId") String vhost, @Param("authHdr") String authHdr);
+ }
+
+ @Test
+ public void parameterizedHeaderExpandApi() throws Exception {
+ List md = contract.parseAndValidatateMetadata(ParameterizedHeaderExpandApi.class);
+
+ assertThat(md).hasSize(1);
+
+ assertThat(md.get(0).configKey())
+ .isEqualTo("ParameterizedHeaderExpandApi#getZone(String,String)");
+ assertThat(md.get(0).returnType())
+ .isEqualTo(String.class);
+ assertThat(md.get(0).template())
+ .hasHeaders(entry("Authorization", asList("{authHdr}")), entry("Accept", asList("application/json")));
+ // Ensure that the authHdr expansion was properly detected and did not create a formParam
+ assertThat(md.get(0).formParams())
+ .isEmpty();
+ }
+
+ @Test
+ public void parameterizedHeaderNotStartingWithCurlyBraceExpandApi() throws Exception {
+ List