diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..fe83f7149
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,68 @@
+# 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
+
+# encrypted values
+*.asc
\ No newline at end of file
diff --git a/.mvn/jvm.config b/.mvn/jvm.config
new file mode 100644
index 000000000..0e7dabeff
--- /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 000000000..8b1378917
--- /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 000000000..c6feb8bb6
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 000000000..c9023edfe
--- /dev/null
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1 @@
+distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.4/apache-maven-3.5.4-bin.zip
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 000000000..6aa329ecd
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,40 @@
+dist: xenial
+sudo: false
+language: java
+
+cache:
+ directories:
+ - $HOME/.m2
+
+jdk:
+ - openjdk8
+ - openjdk11
+
+before_install: ./travis/sign.sh
+
+script:
+ - ./mvnw clean install -B
+ # fail build if there are any local changes to sources
+ - ./travis/no-git-changes.sh
+
+jobs:
+ include:
+ - stage: snapshot
+ name: "Deploy Snapshot to OSSRH"
+ if: branch = master AND type != pull_request AND commit_message !~ /^(prepare release ([0-9\.]+))$/
+ jdk: openjdk8
+ install: true
+ script:
+ - ./mvnw -B -nsu -s ./travis/settings.xml -P release -pl -:feign-benchmark -DskipTests=true deploy
+ - stage: release
+ name: "Release to OSSRH and Central"
+ if: tag =~ /^[0-9\.]+$/
+ jdk: openjdk8
+ install: true
+ script:
+ - ./mvnw -B -nsu -s ./travis/settings.xml -P release -pl -:feign-benchmark -DskipTests=true deploy
+
+env:
+ global:
+ # Ex. travis encrypt GITHUB_TOKEN=token_for_tests
+ - secure: "H4PuppuPE3lkvVQ1osulhgWeZmpIkDKj/z74lx4MUeDPNtcuqpwmTVWtL5Zyjf8CxlALX2djx4RIBshaQAu4GtKarPLONinNLZ/TCtoK8dF08/ESxLEiLQzwGkS+geWoEFiZncB5Px2T7ZbUfVFO3crVY9CLn35znR8k1uidocL0JlyVPGwCwuBxFmDhs3BZh3JvbwSikAVRvlCRU6BbREFQbSK1EamuUju/rlo+dx7W5tiiuEJJ50c8vpgatTFyy821YP82fMRrhuBDpS4/rsL9DmLhQTEbCjZW+22DhEFPRlo0XIfidC7APybXnu3oO+jFuGaFKiQdy7sjB03g/Bz5H7jAIAkbl8UpbjN+IoeUU/OgMuBYf5wJjPDYUEdI3CXqywPn0xYZwVsOcSg+UkQGYdW9ux/U+nKsYLXLWWhst2QMFzbmO94KCrpgCW4mshr/5WP4XU6cEJwDsKMAUPWuOk0KMMjIufSgvPvteWZwT9akZwzEMuGaUQ5kLr1X6xTPv1cKXTreitaoOLQs28kmPVfTwVEdareaSVXcRqeflJJBSXkAgBqGhV5CAEUaUgt9/QD0Jj5RGyRPllFcydXVLTPeg62X/L5COswlvJhPkvfNnkbMpDQZYojKKPmAf+UqZJmVYPpOoNEXygldueKeunWkna/wYkMj0YnOkM8="
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 000000000..0069dc51f
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,303 @@
+### Version 10.6
+* Remove java8 module (#1086)
+* Add composed Spring annotations support (#1090)
+* Generate mocked clients for tests from feign interfaces (#1092)
+
+### Version 10.5
+* Add Apache Http 5 Client (#1065)
+* Updating Apache HttpClient to 4.5.10 (#1080) (#1081)
+* Spring4 contract (#1069)
+* Declarative contracts (#1060)
+
+### Version 10.4
+* Adding support for JDK Proxy (#1045)
+* Add Google HTTP Client support (#1057)
+
+### Version 10.3
+* Upgrade dependencies with security vunerabilities (#997 #1010 #1011 #1024 #1025 #1031 #1032)
+* Parse Retry-After header responses that include decimal points (#980)
+* Fine-grained HTTP error exceptions with client and server errors (#854)
+* Adds support for per request timeout options (#970)
+* Unwrap RetryableException and throw cause (#737)
+* JacksonEncoder avoids intermediate String request body (#989)
+* Respect decode404 flag and decode 404 response body (#1012)
+* Maintain user-given order for header values (#1009)
+
+### Version 10.1
+* Refactoring RequestTemplate to RFC6570 (#778)
+* Allow JAXB context caching in factory (#761)
+* Reactive Wrapper Support (#795)
+* Introduced native http2 client using Java 11 (#806)
+* Unwrap RetryableException and throw cause (#737)
+* Supports PATCH without a body paramter (#824)
+* Feign-Ribbon integration now depends on Ribbon 2.3.0, updated from Ribbon 2.1.1 (#826)
+
+### Version 10.0
+* Feign baseline is now JDK 8
+ - Feign is now being built and tested with OpenJDK 11 as well. Releases and code base will use JDK 8, we are just testing compatibility with JDK 11.
+* Removed @Deprecated methods marked for removal on feign 10.
+* `RetryException` includes the `Method` used for the offending `Request`.
+* `Response` objects now contain the `Request` used.
+
+### 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 Builder exceptionPropagationPolicy(ExceptionPropagationPolicy propagationPolicy) {
+ this.propagationPolicy = propagationPolicy;
+ 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, propagationPolicy);
+ 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 000000000..669f6cb27
--- /dev/null
+++ b/core/src/main/java/feign/FeignException.java
@@ -0,0 +1,450 @@
+/**
+ * Copyright 2012-2019 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.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.Buffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.util.Collection;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import static feign.Util.UTF_8;
+import static feign.Util.checkNotNull;
+import static java.lang.String.format;
+
+/**
+ * Origin exception type for all Http Apis.
+ */
+public class FeignException extends RuntimeException {
+
+ private static final String EXCEPTION_MESSAGE_TEMPLATE_NULL_REQUEST =
+ "request should not be null";
+ private static final long serialVersionUID = 0;
+ private int status;
+ private byte[] content;
+ private Request request;
+
+ protected FeignException(int status, String message, Throwable cause) {
+ super(message, cause);
+ this.status = status;
+ this.request = null;
+ }
+
+ protected FeignException(int status, String message, Throwable cause, byte[] content) {
+ super(message, cause);
+ this.status = status;
+ this.content = content;
+ this.request = null;
+ }
+
+ protected FeignException(int status, String message) {
+ super(message);
+ this.status = status;
+ this.request = null;
+ }
+
+ protected FeignException(int status, String message, byte[] content) {
+ super(message);
+ this.status = status;
+ this.content = content;
+ this.request = null;
+ }
+
+ protected FeignException(int status, String message, Request request, Throwable cause) {
+ super(message, cause);
+ this.status = status;
+ this.request = checkRequestNotNull(request);
+ }
+
+ protected FeignException(int status, String message, Request request, Throwable cause,
+ byte[] content) {
+ super(message, cause);
+ this.status = status;
+ this.content = content;
+ this.request = checkRequestNotNull(request);
+ }
+
+ protected FeignException(int status, String message, Request request) {
+ super(message);
+ this.status = status;
+ this.request = checkRequestNotNull(request);
+ }
+
+ protected FeignException(int status, String message, Request request, byte[] content) {
+ super(message);
+ this.status = status;
+ this.content = content;
+ this.request = checkRequestNotNull(request);
+ }
+
+ private Request checkRequestNotNull(Request request) {
+ return checkNotNull(request, EXCEPTION_MESSAGE_TEMPLATE_NULL_REQUEST);
+ }
+
+ public int status() {
+ return this.status;
+ }
+
+ public byte[] content() {
+ return this.content;
+ }
+
+ public Request request() {
+ return this.request;
+ }
+
+ public boolean hasRequest() {
+ return (this.request != null);
+ }
+
+ public String contentUTF8() {
+ if (content != null) {
+ return new String(content, UTF_8);
+ } else {
+ return "";
+ }
+ }
+
+ static FeignException errorReading(Request request, Response response, IOException cause) {
+ return new FeignException(
+ response.status(),
+ format("%s reading %s %s", cause.getMessage(), request.httpMethod(), request.url()),
+ request,
+ cause,
+ request.requestBody().asBytes());
+ }
+
+ public static FeignException errorStatus(String methodKey, Response response) {
+
+ byte[] body = {};
+ try {
+ if (response.body() != null) {
+ body = Util.toByteArray(response.body().asInputStream());
+ }
+ } catch (IOException ignored) { // NOPMD
+ }
+
+ String message = new FeignExceptionMessageBuilder()
+ .withResponse(response)
+ .withMethodKey(methodKey)
+ .withBody(body).build();
+
+ return errorStatus(response.status(), message, response.request(), body);
+ }
+
+ private static FeignException errorStatus(int status,
+ String message,
+ Request request,
+ byte[] body) {
+ if (isClientError(status)) {
+ return clientErrorStatus(status, message, request, body);
+ }
+ if (isServerError(status)) {
+ return serverErrorStatus(status, message, request, body);
+ }
+ return new FeignException(status, message, request, body);
+ }
+
+ private static boolean isClientError(int status) {
+ return status >= 400 && status < 500;
+ }
+
+ private static FeignClientException clientErrorStatus(int status,
+ String message,
+ Request request,
+ byte[] body) {
+ switch (status) {
+ case 400:
+ return new BadRequest(message, request, body);
+ case 401:
+ return new Unauthorized(message, request, body);
+ case 403:
+ return new Forbidden(message, request, body);
+ case 404:
+ return new NotFound(message, request, body);
+ case 405:
+ return new MethodNotAllowed(message, request, body);
+ case 406:
+ return new NotAcceptable(message, request, body);
+ case 409:
+ return new Conflict(message, request, body);
+ case 410:
+ return new Gone(message, request, body);
+ case 415:
+ return new UnsupportedMediaType(message, request, body);
+ case 429:
+ return new TooManyRequests(message, request, body);
+ case 422:
+ return new UnprocessableEntity(message, request, body);
+ default:
+ return new FeignClientException(status, message, request, body);
+ }
+ }
+
+ private static boolean isServerError(int status) {
+ return status >= 500 && status <= 599;
+ }
+
+ private static FeignServerException serverErrorStatus(int status,
+ String message,
+ Request request,
+ byte[] body) {
+ switch (status) {
+ case 500:
+ return new InternalServerError(message, request, body);
+ case 501:
+ return new NotImplemented(message, request, body);
+ case 502:
+ return new BadGateway(message, request, body);
+ case 503:
+ return new ServiceUnavailable(message, request, body);
+ case 504:
+ return new GatewayTimeout(message, request, body);
+ default:
+ return new FeignServerException(status, message, request, body);
+ }
+ }
+
+ static FeignException errorExecuting(Request request, IOException cause) {
+ return new RetryableException(
+ -1,
+ format("%s executing %s %s", cause.getMessage(), request.httpMethod(), request.url()),
+ request.httpMethod(),
+ cause,
+ null, request);
+ }
+
+ public static class FeignClientException extends FeignException {
+ public FeignClientException(int status, String message, Request request, byte[] body) {
+ super(status, message, request, body);
+ }
+ }
+
+
+ public static class BadRequest extends FeignClientException {
+ public BadRequest(String message, Request request, byte[] body) {
+ super(400, message, request, body);
+ }
+ }
+
+
+ public static class Unauthorized extends FeignClientException {
+ public Unauthorized(String message, Request request, byte[] body) {
+ super(401, message, request, body);
+ }
+ }
+
+
+ public static class Forbidden extends FeignClientException {
+ public Forbidden(String message, Request request, byte[] body) {
+ super(403, message, request, body);
+ }
+ }
+
+
+ public static class NotFound extends FeignClientException {
+ public NotFound(String message, Request request, byte[] body) {
+ super(404, message, request, body);
+ }
+ }
+
+
+ public static class MethodNotAllowed extends FeignClientException {
+ public MethodNotAllowed(String message, Request request, byte[] body) {
+ super(405, message, request, body);
+ }
+ }
+
+
+ public static class NotAcceptable extends FeignClientException {
+ public NotAcceptable(String message, Request request, byte[] body) {
+ super(406, message, request, body);
+ }
+ }
+
+
+ public static class Conflict extends FeignClientException {
+ public Conflict(String message, Request request, byte[] body) {
+ super(409, message, request, body);
+ }
+ }
+
+
+ public static class Gone extends FeignClientException {
+ public Gone(String message, Request request, byte[] body) {
+ super(410, message, request, body);
+ }
+ }
+
+
+ public static class UnsupportedMediaType extends FeignClientException {
+ public UnsupportedMediaType(String message, Request request, byte[] body) {
+ super(415, message, request, body);
+ }
+ }
+
+
+ public static class TooManyRequests extends FeignClientException {
+ public TooManyRequests(String message, Request request, byte[] body) {
+ super(429, message, request, body);
+ }
+ }
+
+
+ public static class UnprocessableEntity extends FeignClientException {
+ public UnprocessableEntity(String message, Request request, byte[] body) {
+ super(422, message, request, body);
+ }
+ }
+
+
+ public static class FeignServerException extends FeignException {
+ public FeignServerException(int status, String message, Request request, byte[] body) {
+ super(status, message, request, body);
+ }
+ }
+
+
+ public static class InternalServerError extends FeignServerException {
+ public InternalServerError(String message, Request request, byte[] body) {
+ super(500, message, request, body);
+ }
+ }
+
+
+ public static class NotImplemented extends FeignServerException {
+ public NotImplemented(String message, Request request, byte[] body) {
+ super(501, message, request, body);
+ }
+ }
+
+
+ public static class BadGateway extends FeignServerException {
+ public BadGateway(String message, Request request, byte[] body) {
+ super(502, message, request, body);
+ }
+ }
+
+
+ public static class ServiceUnavailable extends FeignServerException {
+ public ServiceUnavailable(String message, Request request, byte[] body) {
+ super(503, message, request, body);
+ }
+ }
+
+
+ public static class GatewayTimeout extends FeignServerException {
+ public GatewayTimeout(String message, Request request, byte[] body) {
+ super(504, message, request, body);
+ }
+ }
+
+
+ private static class FeignExceptionMessageBuilder {
+
+ private static final int MAX_BODY_BYTES_LENGTH = 400;
+ private static final int MAX_BODY_CHARS_LENGTH = 200;
+
+ private Response response;
+
+ private byte[] body;
+ private String methodKey;
+
+ public FeignExceptionMessageBuilder withResponse(Response response) {
+ this.response = response;
+ return this;
+ }
+
+ public FeignExceptionMessageBuilder withBody(byte[] body) {
+ this.body = body;
+ return this;
+ }
+
+ public FeignExceptionMessageBuilder withMethodKey(String methodKey) {
+ this.methodKey = methodKey;
+ return this;
+ }
+
+ public String build() {
+ StringBuilder result = new StringBuilder();
+
+ if (response.reason() != null) {
+ result.append(format("[%d %s]", response.status(), response.reason()));
+ } else {
+ result.append(format("[%d]", response.status()));
+ }
+ result.append(format(" during [%s] to [%s] [%s]", response.request().httpMethod(),
+ response.request().url(), methodKey));
+
+ result.append(format(": [%s]", getBodyAsString(body, response.headers())));
+
+ return result.toString();
+ }
+
+ private static String getBodyAsString(byte[] body, Map> headers) {
+ Charset charset = getResponseCharset(headers);
+ if (charset == null) {
+ charset = Util.UTF_8;
+ }
+ return getResponseBody(body, charset);
+ }
+
+ private static String getResponseBody(byte[] body, Charset charset) {
+ if (body.length < MAX_BODY_BYTES_LENGTH) {
+ return new String(body, charset);
+ }
+ return getResponseBodyPreview(body, charset);
+ }
+
+ private static String getResponseBodyPreview(byte[] body, Charset charset) {
+ try {
+ Reader reader = new InputStreamReader(new ByteArrayInputStream(body), charset);
+ CharBuffer result = CharBuffer.allocate(MAX_BODY_CHARS_LENGTH);
+
+ reader.read(result);
+ reader.close();
+ ((Buffer) result).flip();
+ return result.toString() + "... (" + body.length + " bytes)";
+ } catch (IOException e) {
+ return e.toString() + ", failed to parse response";
+ }
+ }
+
+ private static Charset getResponseCharset(Map> headers) {
+
+ Collection strings = headers.get("content-type");
+ if (strings == null || strings.size() == 0) {
+ return null;
+ }
+
+ Pattern pattern = Pattern.compile("charset=([^\\s])");
+ Matcher matcher = pattern.matcher(strings.iterator().next());
+ if (!matcher.lookingAt()) {
+ return null;
+ }
+
+ String group = matcher.group(1);
+ if (!Charset.isSupported(group)) {
+ return null;
+ }
+ return Charset.forName(group);
+
+ }
+ }
+}
diff --git a/core/src/main/java/feign/HeaderMap.java b/core/src/main/java/feign/HeaderMap.java
new file mode 100644
index 000000000..2f06879d1
--- /dev/null
+++ b/core/src/main/java/feign/HeaderMap.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright 2012-2019 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 000000000..c48581fa8
--- /dev/null
+++ b/core/src/main/java/feign/Headers.java
@@ -0,0 +1,78 @@
+/**
+ * Copyright 2012-2019 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.
+ *
+ *
+ */
+@Target({METHOD, TYPE})
+@Retention(RUNTIME)
+public @interface Headers {
+
+ String[] value();
+}
diff --git a/core/src/main/java/feign/InvocationHandlerFactory.java b/core/src/main/java/feign/InvocationHandlerFactory.java
new file mode 100644
index 000000000..73f4a84e2
--- /dev/null
+++ b/core/src/main/java/feign/InvocationHandlerFactory.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright 2012-2019 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.util.Map;
+
+/**
+ * Controls reflective method dispatch.
+ */
+public interface InvocationHandlerFactory {
+
+ InvocationHandler create(Target target, Map dispatch);
+
+ /**
+ * Like {@link InvocationHandler#invoke(Object, java.lang.reflect.Method, Object[])}, except for a
+ * single method.
+ */
+ interface MethodHandler {
+
+ Object invoke(Object[] argv) throws Throwable;
+ }
+
+ static final class Default implements InvocationHandlerFactory {
+
+ @Override
+ public InvocationHandler create(Target target, Map dispatch) {
+ return new ReflectiveFeign.FeignInvocationHandler(target, dispatch);
+ }
+ }
+}
diff --git a/core/src/main/java/feign/Logger.java b/core/src/main/java/feign/Logger.java
new file mode 100644
index 000000000..7b5520ae5
--- /dev/null
+++ b/core/src/main/java/feign/Logger.java
@@ -0,0 +1,272 @@
+/**
+ * Copyright 2012-2019 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.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.logging.FileHandler;
+import java.util.logging.LogRecord;
+import java.util.logging.SimpleFormatter;
+import static feign.Util.UTF_8;
+import static feign.Util.decodeOrDefault;
+import static feign.Util.valuesOrEmpty;
+
+/**
+ * Simple logging abstraction for debug messages. Adapted from {@code retrofit.RestAdapter.Log}.
+ */
+public abstract class Logger {
+
+ protected static String methodTag(String configKey) {
+ return new StringBuilder().append('[').append(configKey.substring(0, configKey.indexOf('(')))
+ .append("] ").toString();
+ }
+
+ /**
+ * Override to log requests and responses using your own implementation. Messages will be http
+ * request and response text.
+ *
+ * @param configKey value of {@link Feign#configKey(Class, java.lang.reflect.Method)}
+ * @param format {@link java.util.Formatter format string}
+ * @param args arguments applied to {@code format}
+ */
+ protected abstract void log(String configKey, String format, Object... args);
+
+ protected void logRequest(String configKey, Level logLevel, Request request) {
+ log(configKey, "---> %s %s HTTP/1.1", request.httpMethod().name(), request.url());
+ if (logLevel.ordinal() >= Level.HEADERS.ordinal()) {
+
+ for (String field : request.headers().keySet()) {
+ for (String value : valuesOrEmpty(request.headers(), field)) {
+ log(configKey, "%s: %s", field, value);
+ }
+ }
+
+ int bodyLength = 0;
+ if (request.requestBody().asBytes() != null) {
+ bodyLength = request.requestBody().asBytes().length;
+ if (logLevel.ordinal() >= Level.FULL.ordinal()) {
+ String bodyText =
+ request.charset() != null
+ ? new String(request.requestBody().asBytes(), request.charset())
+ : null;
+ log(configKey, ""); // CRLF
+ log(configKey, "%s", bodyText != null ? bodyText : "Binary data");
+ }
+ }
+ log(configKey, "---> END HTTP (%s-byte body)", bodyLength);
+ }
+ }
+
+ protected void logRetry(String configKey, Level logLevel) {
+ log(configKey, "---> RETRYING");
+ }
+
+ protected Response logAndRebufferResponse(String configKey,
+ Level logLevel,
+ Response response,
+ long elapsedTime)
+ throws IOException {
+ String reason =
+ response.reason() != null && logLevel.compareTo(Level.NONE) > 0 ? " " + response.reason()
+ : "";
+ int status = response.status();
+ log(configKey, "<--- HTTP/1.1 %s%s (%sms)", status, reason, elapsedTime);
+ if (logLevel.ordinal() >= Level.HEADERS.ordinal()) {
+
+ for (String field : response.headers().keySet()) {
+ for (String value : valuesOrEmpty(response.headers(), field)) {
+ log(configKey, "%s: %s", field, value);
+ }
+ }
+
+ int bodyLength = 0;
+ if (response.body() != null && !(status == 204 || status == 205)) {
+ // HTTP 204 No Content "...response MUST NOT include a message-body"
+ // HTTP 205 Reset Content "...response MUST NOT include an entity"
+ if (logLevel.ordinal() >= Level.FULL.ordinal()) {
+ log(configKey, ""); // CRLF
+ }
+ byte[] bodyData = Util.toByteArray(response.body().asInputStream());
+ bodyLength = bodyData.length;
+ if (logLevel.ordinal() >= Level.FULL.ordinal() && bodyLength > 0) {
+ log(configKey, "%s", decodeOrDefault(bodyData, UTF_8, "Binary data"));
+ }
+ log(configKey, "<--- END HTTP (%s-byte body)", bodyLength);
+ return response.toBuilder().body(bodyData).build();
+ } else {
+ log(configKey, "<--- END HTTP (%s-byte body)", bodyLength);
+ }
+ }
+ return response;
+ }
+
+ protected IOException logIOException(String configKey,
+ Level logLevel,
+ IOException ioe,
+ long elapsedTime) {
+ log(configKey, "<--- ERROR %s: %s (%sms)", ioe.getClass().getSimpleName(), ioe.getMessage(),
+ elapsedTime);
+ if (logLevel.ordinal() >= Level.FULL.ordinal()) {
+ StringWriter sw = new StringWriter();
+ ioe.printStackTrace(new PrintWriter(sw));
+ log(configKey, "%s", sw.toString());
+ log(configKey, "<--- END ERROR");
+ }
+ return ioe;
+ }
+
+ /**
+ * Controls the level of logging.
+ */
+ public enum Level {
+ /**
+ * No logging.
+ */
+ NONE,
+ /**
+ * Log only the request method and URL and the response status code and execution time.
+ */
+ BASIC,
+ /**
+ * Log the basic information along with request and response headers.
+ */
+ HEADERS,
+ /**
+ * Log the headers, body, and metadata for both requests and responses.
+ */
+ FULL
+ }
+
+ /**
+ * Logs to System.err.
+ */
+ public static class ErrorLogger extends Logger {
+ @Override
+ protected void log(String configKey, String format, Object... args) {
+ System.err.printf(methodTag(configKey) + format + "%n", args);
+ }
+ }
+
+ /**
+ * Logs to the category {@link Logger} at {@link java.util.logging.Level#FINE}, if loggable.
+ */
+ public static class JavaLogger extends Logger {
+
+ final java.util.logging.Logger logger;
+
+ /**
+ * @deprecated Use {@link #JavaLogger(String)} or {@link #JavaLogger(Class)} instead.
+ *
+ * This constructor can be used to create just one logger. Example =
+ * {@code Logger.JavaLogger().appendToFile("logs/first.log")}
+ *
+ * If you create multiple loggers for multiple clients and provide different files
+ * to write log - you'll have unexpected behavior - all clients will write same log
+ * to each file.
+ *
+ * That's why this constructor will be removed in future.
+ */
+ @Deprecated
+ public JavaLogger() {
+ logger = java.util.logging.Logger.getLogger(Logger.class.getName());
+ }
+
+ /**
+ * Constructor for JavaLogger class
+ *
+ * @param loggerName a name for the logger. This should be a dot-separated name and should
+ * normally be based on the package name or class name of the subsystem, such as java.net
+ * or javax.swing
+ */
+ public JavaLogger(String loggerName) {
+ logger = java.util.logging.Logger.getLogger(loggerName);
+ }
+
+ /**
+ * Constructor for JavaLogger class
+ *
+ * @param clazz the returned logger will be named after clazz
+ */
+ public JavaLogger(Class> clazz) {
+ logger = java.util.logging.Logger.getLogger(clazz.getName());
+ }
+
+ @Override
+ protected void logRequest(String configKey, Level logLevel, Request request) {
+ if (logger.isLoggable(java.util.logging.Level.FINE)) {
+ super.logRequest(configKey, logLevel, request);
+ }
+ }
+
+ @Override
+ protected Response logAndRebufferResponse(String configKey,
+ Level logLevel,
+ Response response,
+ long elapsedTime)
+ throws IOException {
+ if (logger.isLoggable(java.util.logging.Level.FINE)) {
+ return super.logAndRebufferResponse(configKey, logLevel, response, elapsedTime);
+ }
+ return response;
+ }
+
+ @Override
+ protected void log(String configKey, String format, Object... args) {
+ if (logger.isLoggable(java.util.logging.Level.FINE)) {
+ logger.fine(String.format(methodTag(configKey) + format, args));
+ }
+ }
+
+ /**
+ * Helper that configures java.util.logging to sanely log messages at FINE level without
+ * additional formatting.
+ */
+ public JavaLogger appendToFile(String logfile) {
+ logger.setLevel(java.util.logging.Level.FINE);
+ try {
+ FileHandler handler = new FileHandler(logfile, true);
+ handler.setFormatter(new SimpleFormatter() {
+ @Override
+ public String format(LogRecord record) {
+ return String.format("%s%n", record.getMessage()); // NOPMD
+ }
+ });
+ logger.addHandler(handler);
+ } catch (IOException e) {
+ throw new IllegalStateException("Could not add file handler.", e);
+ }
+ return this;
+ }
+ }
+
+ public static class NoOpLogger extends Logger {
+
+ @Override
+ protected void logRequest(String configKey, Level logLevel, Request request) {}
+
+ @Override
+ protected Response logAndRebufferResponse(String configKey,
+ Level logLevel,
+ Response response,
+ long elapsedTime)
+ throws IOException {
+ return response;
+ }
+
+ @Override
+ protected void log(String configKey, String format, Object... args) {}
+ }
+}
diff --git a/core/src/main/java/feign/MethodMetadata.java b/core/src/main/java/feign/MethodMetadata.java
new file mode 100644
index 000000000..84e7db070
--- /dev/null
+++ b/core/src/main/java/feign/MethodMetadata.java
@@ -0,0 +1,243 @@
+/**
+ * Copyright 2012-2019 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.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.util.*;
+import feign.Param.Expander;
+
+public final class MethodMetadata implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+ private String configKey;
+ private transient Type returnType;
+ private Integer urlIndex;
+ private Integer bodyIndex;
+ private Integer headerMapIndex;
+ private Integer queryMapIndex;
+ private boolean queryMapEncoded;
+ private transient Type bodyType;
+ private RequestTemplate template = new RequestTemplate();
+ private List formParams = new ArrayList();
+ private Map> indexToName =
+ new LinkedHashMap>();
+ private Map> indexToExpanderClass =
+ new LinkedHashMap>();
+ private Map indexToEncoded = new LinkedHashMap();
+ private transient Map indexToExpander;
+ private BitSet parameterToIgnore = new BitSet();
+ private boolean ignored;
+ private transient Class> targetType;
+ private transient Method method;
+
+ MethodMetadata() {
+ template.methodMetadata(this);
+ }
+
+ /**
+ * Used as a reference to this method. For example, {@link Logger#log(String, String, Object...)
+ * logging} or {@link ReflectiveFeign reflective dispatch}.
+ *
+ * @see Feign#configKey(Class, java.lang.reflect.Method)
+ */
+ public String configKey() {
+ return configKey;
+ }
+
+ public MethodMetadata configKey(String configKey) {
+ this.configKey = configKey;
+ return this;
+ }
+
+ public Type returnType() {
+ return returnType;
+ }
+
+ public MethodMetadata returnType(Type returnType) {
+ this.returnType = returnType;
+ return this;
+ }
+
+ public Integer urlIndex() {
+ return urlIndex;
+ }
+
+ public MethodMetadata urlIndex(Integer urlIndex) {
+ this.urlIndex = urlIndex;
+ return this;
+ }
+
+ public Integer bodyIndex() {
+ return bodyIndex;
+ }
+
+ public MethodMetadata bodyIndex(Integer bodyIndex) {
+ this.bodyIndex = bodyIndex;
+ return this;
+ }
+
+ public Integer headerMapIndex() {
+ return headerMapIndex;
+ }
+
+ public MethodMetadata headerMapIndex(Integer headerMapIndex) {
+ this.headerMapIndex = headerMapIndex;
+ return this;
+ }
+
+ public Integer queryMapIndex() {
+ return queryMapIndex;
+ }
+
+ public MethodMetadata queryMapIndex(Integer queryMapIndex) {
+ this.queryMapIndex = queryMapIndex;
+ return this;
+ }
+
+ public boolean queryMapEncoded() {
+ return queryMapEncoded;
+ }
+
+ public MethodMetadata queryMapEncoded(boolean queryMapEncoded) {
+ this.queryMapEncoded = queryMapEncoded;
+ return this;
+ }
+
+ /**
+ * Type corresponding to {@link #bodyIndex()}.
+ */
+ public Type bodyType() {
+ return bodyType;
+ }
+
+ public MethodMetadata bodyType(Type bodyType) {
+ this.bodyType = bodyType;
+ return this;
+ }
+
+ public RequestTemplate template() {
+ return template;
+ }
+
+ public List formParams() {
+ return formParams;
+ }
+
+ public Map> indexToName() {
+ return indexToName;
+ }
+
+ public Map indexToEncoded() {
+ return indexToEncoded;
+ }
+
+ /**
+ * If {@link #indexToExpander} is null, classes here will be instantiated by newInstance.
+ */
+ public Map> indexToExpanderClass() {
+ return indexToExpanderClass;
+ }
+
+ /**
+ * After {@link #indexToExpanderClass} is populated, this is set by contracts that support runtime
+ * injection.
+ */
+ public MethodMetadata indexToExpander(Map indexToExpander) {
+ this.indexToExpander = indexToExpander;
+ return this;
+ }
+
+ /**
+ * When not null, this value will be used instead of {@link #indexToExpander()}.
+ */
+ public Map indexToExpander() {
+ return indexToExpander;
+ }
+
+ /**
+ * @param i individual parameter that should be ignored
+ * @return this instance
+ */
+ public MethodMetadata ignoreParamater(int i) {
+ this.parameterToIgnore.set(i);
+ return this;
+ }
+
+ public BitSet parameterToIgnore() {
+ return parameterToIgnore;
+ }
+
+ public MethodMetadata parameterToIgnore(BitSet parameterToIgnore) {
+ this.parameterToIgnore = parameterToIgnore;
+ return this;
+ }
+
+ /**
+ * @param i individual parameter to check if should be ignored
+ * @return true when field should not be processed by feign
+ */
+ public boolean shouldIgnoreParamater(int i) {
+ return parameterToIgnore.get(i);
+ }
+
+ /**
+ * @param index
+ * @return true if the parameter {@code index} was already consumed by a any
+ * {@link MethodMetadata} holder
+ */
+ public boolean isAlreadyProcessed(Integer index) {
+ return index.equals(urlIndex)
+ || index.equals(bodyIndex)
+ || index.equals(headerMapIndex)
+ || index.equals(queryMapIndex)
+ || indexToName.containsKey(index)
+ || indexToExpanderClass.containsKey(index)
+ || indexToEncoded.containsKey(index)
+ || (indexToExpander != null && indexToExpander.containsKey(index))
+ || parameterToIgnore.get(index);
+ }
+
+ public void ignoreMethod() {
+ this.ignored = true;
+ }
+
+ public boolean isIgnored() {
+ return ignored;
+ }
+
+ @Experimental
+ public MethodMetadata targetType(Class> targetType) {
+ this.targetType = targetType;
+ return this;
+ }
+
+ @Experimental
+ public Class> targetType() {
+ return targetType;
+ }
+
+ @Experimental
+ public MethodMetadata method(Method method) {
+ this.method = method;
+ return this;
+ }
+
+ @Experimental
+ public Method method() {
+ return method;
+ }
+
+}
diff --git a/core/src/main/java/feign/Param.java b/core/src/main/java/feign/Param.java
new file mode 100644
index 000000000..e0045ae7a
--- /dev/null
+++ b/core/src/main/java/feign/Param.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright 2012-2019 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.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * A named template parameter applied to {@link Headers}, {@linkplain RequestLine} or
+ * {@linkplain Body}
+ */
+@Retention(RUNTIME)
+@java.lang.annotation.Target(PARAMETER)
+public @interface Param {
+
+ /**
+ * The name of the template parameter.
+ */
+ String value();
+
+ /**
+ * How to expand the value of this parameter, if {@link ToStringExpander} isn't adequate.
+ */
+ Class extends Expander> expander() default ToStringExpander.class;
+
+ /**
+ * {@code encoded} has been maintained for backward compatibility and should be deprecated. We no
+ * longer need it as values that are already pct-encoded should be identified during expansion and
+ * passed through without any changes
+ *
+ * @see QueryMap#encoded
+ * @deprecated
+ */
+ boolean encoded() default false;
+
+ interface Expander {
+
+ /**
+ * Expands the value into a string. Does not accept or return null.
+ */
+ String expand(Object value);
+ }
+
+ final class ToStringExpander implements Expander {
+
+ @Override
+ public String expand(Object value) {
+ return value.toString();
+ }
+ }
+}
diff --git a/core/src/main/java/feign/QueryMap.java b/core/src/main/java/feign/QueryMap.java
new file mode 100644
index 000000000..c50a46cb5
--- /dev/null
+++ b/core/src/main/java/feign/QueryMap.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright 2012-2019 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 query parameters, where the keys
+ * are Strings that are the parameter names and the values are the parameter values. The queries
+ * specified by the map will be applied to the request after all other processing, and will take
+ * precedence over any previously specified query parameters. It is not necessary to reference the
+ * parameter map as a variable.
+ *
+ *
+ *
+ *
+ * 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 000000000..f7bb8fa7d
--- /dev/null
+++ b/core/src/main/java/feign/QueryMapEncoder.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright 2012-2019 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.querymap.FieldQueryMapEncoder;
+import feign.querymap.BeanQueryMapEncoder;
+import java.util.Map;
+
+/**
+ * A QueryMapEncoder encodes Objects into maps of query parameter names to values.
+ *
+ * @see FieldQueryMapEncoder
+ * @see BeanQueryMapEncoder
+ *
+ */
+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);
+
+ /**
+ * @deprecated use {@link BeanQueryMapEncoder} instead. default encoder uses reflection to inspect
+ * provided objects Fields to expand the objects values into a query string. If you
+ * prefer that the query string be built using getter and setter methods, as defined
+ * in the Java Beans API, please use the {@link BeanQueryMapEncoder}
+ */
+ class Default extends FieldQueryMapEncoder {
+ }
+}
diff --git a/core/src/main/java/feign/ReflectiveFeign.java b/core/src/main/java/feign/ReflectiveFeign.java
new file mode 100644
index 000000000..612e164c1
--- /dev/null
+++ b/core/src/main/java/feign/ReflectiveFeign.java
@@ -0,0 +1,389 @@
+/**
+ * Copyright 2012-2019 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.checkArgument;
+import static feign.Util.checkNotNull;
+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.*;
+import feign.template.UriUtils;
+
+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 target) {
+ List metadata = contract.parseAndValidateMetadata(target.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, target);
+ } else if (md.bodyIndex() != null) {
+ buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
+ } else {
+ buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder, target);
+ }
+ if (md.isIgnored()) {
+ result.put(md.configKey(), args -> {
+ throw new IllegalStateException(md.configKey() + " is not a method handled by feign");
+ });
+ } else {
+ result.put(md.configKey(),
+ factory.create(target, md, buildTemplate, options, decoder, errorDecoder));
+ }
+ }
+ return result;
+ }
+ }
+
+ private static class BuildTemplateByResolvingArgs implements RequestTemplate.Factory {
+
+ private final QueryMapEncoder queryMapEncoder;
+
+ protected final MethodMetadata metadata;
+ protected final Target> target;
+ private final Map indexToExpander = new LinkedHashMap();
+
+ private BuildTemplateByResolvingArgs(MethodMetadata metadata, QueryMapEncoder queryMapEncoder,
+ Target target) {
+ this.metadata = metadata;
+ this.target = target;
+