diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 0000000000..5a423a0278
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,230 @@
+#
+# Copyright 2012-2020 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.
+#
+
+# common executors
+executors:
+ java:
+ parameters:
+ version:
+ description: 'jdk version to use'
+ default: '8'
+ type: string
+ docker:
+ - image: circleci/openjdk:<>
+
+# common commands
+commands:
+ resolve-dependencies:
+ description: 'Download and prepare all dependencies'
+ steps:
+ - run:
+ name: 'Resolving Dependencies'
+ command: |
+ mvn dependency:resolve-plugins go-offline:resolve-dependencies -DskipTests=true
+ verify-formatting:
+ steps:
+ - run:
+ name: 'Verify formatting'
+ command: |
+ scripts/no-git-changes.sh
+ configure-gpg:
+ steps:
+ - run:
+ name: 'Configure GPG keys'
+ command: |
+ echo -e "$GPG_KEY" | gpg --batch --no-tty --import --yes
+ nexus-deploy:
+ steps:
+ - run:
+ name: 'Deploy Core Modules Sonatype'
+ command: |
+ mvn -nsu -s .circleci/settings.xml -P release -pl -:feign-benchmark -DskipTests=true deploy
+ nexus-deploy-jdk11:
+ steps:
+ - run:
+ name: 'Build JDK 11 Release modules locally'
+ command: |
+ mvn -B -nsu -s .circleci/settings.xml -P java11 -pl :feign-java11 -am -DskipTests=true install
+ - run:
+ name: 'Deploy JDK 11 Modules to Sonatype'
+ command: |
+ mvn -B -nsu -s .circleci/settings.xml -P release,java11 -pl :feign-java11 -DskipTests=true deploy
+
+# our job defaults
+defaults: &defaults
+ working_directory: ~/feign
+ environment:
+ # Customize the JVM maximum heap limit
+ MAVEN_OPTS: -Xmx3200m
+
+# branch filters
+master-only: &master-only
+ branches:
+ only: master
+
+tags-only: &tags-only
+ branches:
+ ignore: /.*/
+ tags:
+ only: /.*/
+
+all-branches: &all-branches
+ branches:
+ ignore: master
+ tags:
+ ignore: /.*/
+
+version: 2.1
+
+jobs:
+ test:
+ parameters:
+ jdk:
+ description: 'jdk version to use'
+ default: '8'
+ type: string
+ executor:
+ name: java
+ version: <>
+ <<: *defaults
+ steps:
+ - checkout
+ - restore_cache:
+ keys:
+ - feign-dependencies-{{ checksum "pom.xml" }}
+ - feign-dependencies-
+ - resolve-dependencies
+ - save_cache:
+ paths:
+ - ~/.m2
+ key: feign-dependencies-{{ checksum "pom.xml" }}
+ - run:
+ name: 'Test'
+ command: |
+ mvn -o test
+ - verify-formatting
+
+ deploy:
+ parameters:
+ jdk:
+ description: 'jdk version to use'
+ default: '8'
+ type: string
+ executor:
+ name: java
+ version: <>
+ <<: *defaults
+ steps:
+ - checkout
+ - restore_cache:
+ keys:
+ - feign-dependencies-{{ checksum "pom.xml" }}
+ - feign-dependencies-
+ - resolve-dependencies
+ - configure-gpg
+ - nexus-deploy
+
+ deploy-jdk11:
+ parameters:
+ jdk:
+ description: 'jdk version to use'
+ default: '11'
+ type: string
+ executor:
+ name: java
+ version: <>
+ <<: *defaults
+ steps:
+ - checkout
+ - restore_cache:
+ keys:
+ - feign-dependencies-{{ checksum "pom.xml" }}
+ - feign-dependencies-
+ - resolve-dependencies
+ - configure-gpg
+ - nexus-deploy-jdk11
+
+workflows:
+ version: 2
+ build:
+ jobs:
+ - test:
+ jdk: '8'
+ name: 'jdk 8'
+ filters:
+ <<: *all-branches
+ - test:
+ jdk: '11'
+ name: 'jdk 11'
+ filters:
+ <<: *all-branches
+ - test:
+ jdk: '14-buster'
+ name: 'jdk 14'
+ filters:
+ <<: *all-branches
+
+ snapshot:
+ jobs:
+ - test:
+ jdk: '8'
+ name: 'jdk 8'
+ filters:
+ <<: *master-only
+ - test:
+ jdk: '11'
+ name: 'jdk 11'
+ filters:
+ <<: *master-only
+ - test:
+ jdk: '14-buster'
+ name: 'jdk 14'
+ filters:
+ <<: *master-only
+ - deploy:
+ jdk: '8'
+ name: 'deploy snapshot'
+ requires:
+ - 'jdk 8'
+ - 'jdk 11'
+ - 'jdk 14'
+ context: Sonatype
+ filters:
+ <<: *master-only
+ - deploy-jdk11:
+ jdk: '11'
+ name: 'deploy jdk11 snapshot modules'
+ requires:
+ - 'jdk 11'
+ - 'deploy snapshot'
+ context: Sonatype
+ filters:
+ <<: *master-only
+
+ release:
+ jobs:
+ - deploy:
+ jdk: '8'
+ name: 'release to maven central'
+ context: Sonatype
+ filters:
+ <<: *tags-only
+ - deploy-jdk11:
+ jdk: '11'
+ name: 'release jdk11 artifacts to maven central'
+ requires:
+ - 'release to maven central'
+ context: Sonatype
+ filters:
+ <<: *tags-only
diff --git a/.circleci/settings.xml b/.circleci/settings.xml
new file mode 100644
index 0000000000..b3b4740ad1
--- /dev/null
+++ b/.circleci/settings.xml
@@ -0,0 +1,39 @@
+
+
+
+
+ ossrh
+ ${env.SONATYPE_USER}
+ ${env.SONATYPE_PASSWORD}
+
+
+
+
+ ossrh
+
+ true
+
+
+ ${env.GPG_PASSPHRASE}
+
+
+
+
+
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000..b4a09015cc
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,71 @@
+# 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
+
+# maven versions
+*.versionsBackup
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..c9023edfe7
--- /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/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000000..33cade1b17
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,324 @@
+### Version 10.9
+
+* Configurable to disable streaming mode for Default client by verils (#1182)
+* Overriding query parameter name by boggard (#1184)
+* Internal feign metrics by velo:
+* Dropwizard metrics 5 (#1181)
+* Micrometer (#1188)
+
+### Version 10.8
+
+* async feign variant supporting CompleteableFutures by motinis (#1174)
+* deterministic iterations for Feign mocks by contextshuffling (#1165)
+* Async client for apache http 5 by velo (#1179)
+
+### Version 10.7
+
+* Fix for vunerabilities reported by snky (#1121)
+* Makes iterator compatible with Java iterator expected behavior (#1117)
+* Bump reactive dependencies (#1105)
+* Deprecated `encoded` and add comment (#1108)
+
+### 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 Builder addCapability(Capability capability) {
+ this.capabilities.add(capability);
+ return this;
+ }
+
+ /**
+ * Internal - used to indicate that the decoder should be immediately called
+ */
+ Builder forceDecoding() {
+ this.forceDecoding = true;
+ 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() {
+ Client client = Capability.enrich(this.client, capabilities);
+ Retryer retryer = Capability.enrich(this.retryer, capabilities);
+ List requestInterceptors = this.requestInterceptors.stream()
+ .map(ri -> Capability.enrich(ri, capabilities))
+ .collect(Collectors.toList());
+ Logger logger = Capability.enrich(this.logger, capabilities);
+ Contract contract = Capability.enrich(this.contract, capabilities);
+ Options options = Capability.enrich(this.options, capabilities);
+ Encoder encoder = Capability.enrich(this.encoder, capabilities);
+ Decoder decoder = Capability.enrich(this.decoder, capabilities);
+ InvocationHandlerFactory invocationHandlerFactory =
+ Capability.enrich(this.invocationHandlerFactory, capabilities);
+ QueryMapEncoder queryMapEncoder = Capability.enrich(this.queryMapEncoder, capabilities);
+
+ SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
+ new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
+ logLevel, decode404, closeAfterDecode, propagationPolicy, forceDecoding);
+ ParseHandlersByName handlersByName =
+ new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,
+ errorDecoder, synchronousMethodHandlerFactory);
+ return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
+ }
+ }
+
+ public static class ResponseMappingDecoder implements Decoder {
+
+ private final ResponseMapper mapper;
+ private final Decoder delegate;
+
+ public 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..2e8946f35b
--- /dev/null
+++ b/core/src/main/java/feign/FeignException.java
@@ -0,0 +1,471 @@
+/**
+ * Copyright 2012-2021 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.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Optional;
+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[] responseBody;
+ 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[] responseBody) {
+ super(message, cause);
+ this.status = status;
+ this.responseBody = responseBody;
+ this.request = null;
+ }
+
+ protected FeignException(int status, String message) {
+ super(message);
+ this.status = status;
+ this.request = null;
+ }
+
+ protected FeignException(int status, String message, byte[] responseBody) {
+ super(message);
+ this.status = status;
+ this.responseBody = responseBody;
+ 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[] responseBody) {
+ super(message, cause);
+ this.status = status;
+ this.responseBody = responseBody;
+ 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[] responseBody) {
+ super(message);
+ this.status = status;
+ this.responseBody = responseBody;
+ this.request = checkRequestNotNull(request);
+ }
+
+ private Request checkRequestNotNull(Request request) {
+ return checkNotNull(request, EXCEPTION_MESSAGE_TEMPLATE_NULL_REQUEST);
+ }
+
+ public int status() {
+ return this.status;
+ }
+
+ /**
+ * The Response Body, if present.
+ *
+ * @return the body of the response.
+ * @deprecated use {@link #responseBody()} instead.
+ */
+ @Deprecated
+ public byte[] content() {
+ return this.responseBody;
+ }
+
+ /**
+ * The Response body.
+ *
+ * @return an Optional wrapping the response body.
+ */
+ public Optional responseBody() {
+ if (this.responseBody == null) {
+ return Optional.empty();
+ }
+ return Optional.of(ByteBuffer.wrap(this.responseBody));
+ }
+
+ public Request request() {
+ return this.request;
+ }
+
+ public boolean hasRequest() {
+ return (this.request != null);
+ }
+
+ public String contentUTF8() {
+ if (responseBody != null) {
+ return new String(responseBody, 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.body());
+ }
+
+ 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.isEmpty()) {
+ 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 0000000000..e8d89c7f2c
--- /dev/null
+++ b/core/src/main/java/feign/HeaderMap.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright 2012-2020 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..77dcdc738a
--- /dev/null
+++ b/core/src/main/java/feign/Headers.java
@@ -0,0 +1,78 @@
+/**
+ * Copyright 2012-2020 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 0000000000..6a4811dd28
--- /dev/null
+++ b/core/src/main/java/feign/InvocationHandlerFactory.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright 2012-2020 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 0000000000..454ccd1198
--- /dev/null
+++ b/core/src/main/java/feign/Logger.java
@@ -0,0 +1,269 @@
+/**
+ * Copyright 2012-2020 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.logging.FileHandler;
+import java.util.logging.LogRecord;
+import java.util.logging.SimpleFormatter;
+import static feign.Util.*;
+
+/**
+ * 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.body() != null) {
+ bodyLength = request.length();
+ if (logLevel.ordinal() >= Level.FULL.ordinal()) {
+ String bodyText =
+ request.charset() != null
+ ? new String(request.body(), request.charset())
+ : null;
+ log(configKey, ""); // CRLF
+ log(configKey, "%s", 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/MethodInfo.java b/core/src/main/java/feign/MethodInfo.java
new file mode 100644
index 0000000000..4d3f5777c3
--- /dev/null
+++ b/core/src/main/java/feign/MethodInfo.java
@@ -0,0 +1,58 @@
+/**
+ * Copyright 2012-2020 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.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.concurrent.CompletableFuture;
+
+@Experimental
+class MethodInfo {
+ private final String configKey;
+ private final Type underlyingReturnType;
+ private final boolean asyncReturnType;
+
+ MethodInfo(String configKey, Type underlyingReturnType, boolean asyncReturnType) {
+ this.configKey = configKey;
+ this.underlyingReturnType = underlyingReturnType;
+ this.asyncReturnType = asyncReturnType;
+ }
+
+ MethodInfo(Class> targetType, Method method) {
+ this.configKey = Feign.configKey(targetType, method);
+
+ final Type type = method.getGenericReturnType();
+
+ if (method.getReturnType() != CompletableFuture.class) {
+ this.asyncReturnType = false;
+ this.underlyingReturnType = type;
+ } else {
+ this.asyncReturnType = true;
+ this.underlyingReturnType = ((ParameterizedType) type).getActualTypeArguments()[0];
+ }
+ }
+
+ String configKey() {
+ return configKey;
+ }
+
+ Type underlyingReturnType() {
+ return underlyingReturnType;
+ }
+
+ boolean isAsyncReturnType() {
+ return asyncReturnType;
+ }
+}
diff --git a/core/src/main/java/feign/MethodMetadata.java b/core/src/main/java/feign/MethodMetadata.java
new file mode 100644
index 0000000000..87de5e43f9
--- /dev/null
+++ b/core/src/main/java/feign/MethodMetadata.java
@@ -0,0 +1,254 @@
+/**
+ * Copyright 2012-2020 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 java.util.stream.Collectors;
+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 final RequestTemplate template = new RequestTemplate();
+ private final List formParams = new ArrayList();
+ private final Map> indexToName =
+ new LinkedHashMap>();
+ private final Map> indexToExpanderClass =
+ new LinkedHashMap>();
+ private final Map indexToEncoded = new LinkedHashMap();
+ private transient Map indexToExpander;
+ private BitSet parameterToIgnore = new BitSet();
+ private boolean ignored;
+ private transient Class> targetType;
+ private transient Method method;
+ private transient final List warnings = new ArrayList<>();
+
+ 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;
+ }
+
+ public void addWarning(String warning) {
+ warnings.add(warning);
+ }
+
+ public String warnings() {
+ return warnings.stream()
+ .collect(Collectors.joining("\n- ", "\nWarnings:\n- ", ""));
+ }
+
+}
diff --git a/core/src/main/java/feign/Param.java b/core/src/main/java/feign/Param.java
new file mode 100644
index 0000000000..7501cc29f2
--- /dev/null
+++ b/core/src/main/java/feign/Param.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright 2012-2020 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.*;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * A named template parameter applied to {@link Headers}, {@linkplain RequestLine},
+ * {@linkplain Body}, POJO fields or beans properties when it expanding
+ */
+@Retention(RUNTIME)
+@java.lang.annotation.Target({PARAMETER, FIELD, METHOD})
+public @interface Param {
+
+ /**
+ * The name of the template parameter.
+ */
+ String value() default "";
+
+ /**
+ * 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 0000000000..6515f21d7f
--- /dev/null
+++ b/core/src/main/java/feign/QueryMap.java
@@ -0,0 +1,67 @@
+/**
+ * Copyright 2012-2020 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...)}.
+ */
+@SuppressWarnings("deprecation")
+@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..9551628f80
--- /dev/null
+++ b/core/src/main/java/feign/QueryMapEncoder.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright 2012-2020 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/ReflectiveAsyncFeign.java b/core/src/main/java/feign/ReflectiveAsyncFeign.java
new file mode 100644
index 0000000000..f58fe66aca
--- /dev/null
+++ b/core/src/main/java/feign/ReflectiveAsyncFeign.java
@@ -0,0 +1,137 @@
+/**
+ * Copyright 2012-2020 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.*;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Experimental
+public class ReflectiveAsyncFeign extends AsyncFeign {
+
+ private class AsyncFeignInvocationHandler implements InvocationHandler {
+
+ private final Map methodInfoLookup = new ConcurrentHashMap<>();
+
+ private final Class type;
+ private final T instance;
+ private final C context;
+
+ AsyncFeignInvocationHandler(Class type, T instance, C context) {
+ this.type = type;
+ this.instance = instance;
+ this.context = context;
+ }
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ if ("equals".equals(method.getName()) && method.getParameterCount() == 1) {
+ try {
+ final Object otherHandler =
+ args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0])
+ : null;
+ return equals(otherHandler);
+ } catch (final IllegalArgumentException e) {
+ return false;
+ }
+ } else if ("hashCode".equals(method.getName()) && method.getParameterCount() == 0) {
+ return hashCode();
+ } else if ("toString".equals(method.getName()) && method.getParameterCount() == 0) {
+ return toString();
+ }
+
+ final MethodInfo methodInfo =
+ methodInfoLookup.computeIfAbsent(method, m -> new MethodInfo(type, m));
+
+ setInvocationContext(new AsyncInvocation(context, methodInfo));
+ try {
+ return method.invoke(instance, args);
+ } catch (final InvocationTargetException e) {
+ Throwable cause = e.getCause();
+ if (cause instanceof AsyncJoinException) {
+ cause = cause.getCause();
+ }
+ throw cause;
+ } finally {
+ clearInvocationContext();
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof AsyncFeignInvocationHandler) {
+ final AsyncFeignInvocationHandler> other = (AsyncFeignInvocationHandler>) obj;
+ return instance.equals(other.instance);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return instance.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return instance.toString();
+ }
+ }
+
+ public ReflectiveAsyncFeign(AsyncBuilder asyncBuilder) {
+ super(asyncBuilder);
+ }
+
+ private String getFullMethodName(Class> type, Type retType, Method m) {
+ return retType.getTypeName() + " " + type.toGenericString() + "." + m.getName();
+ }
+
+ @Override
+ protected T wrap(Class type, T instance, C context) {
+ if (!type.isInterface()) {
+ throw new IllegalArgumentException("Type must be an interface: " + type);
+ }
+
+ for (final Method m : type.getMethods()) {
+ final Class> retType = m.getReturnType();
+
+ if (!CompletableFuture.class.isAssignableFrom(retType)) {
+ continue; // synchronous case
+ }
+
+ if (retType != CompletableFuture.class) {
+ throw new IllegalArgumentException("Method return type is not CompleteableFuture: "
+ + getFullMethodName(type, retType, m));
+ }
+
+ final Type genRetType = m.getGenericReturnType();
+
+ if (!ParameterizedType.class.isInstance(genRetType)) {
+ throw new IllegalArgumentException("Method return type is not parameterized: "
+ + getFullMethodName(type, genRetType, m));
+ }
+
+ if (WildcardType.class
+ .isInstance(ParameterizedType.class.cast(genRetType).getActualTypeArguments()[0])) {
+ throw new IllegalArgumentException(
+ "Wildcards are not supported for return-type parameters: "
+ + getFullMethodName(type, genRetType, m));
+ }
+ }
+
+ return type.cast(Proxy.newProxyInstance(type.getClassLoader(), new Class>[] {type},
+ new AsyncFeignInvocationHandler<>(type, instance, context)));
+ }
+}
diff --git a/core/src/main/java/feign/ReflectiveFeign.java b/core/src/main/java/feign/ReflectiveFeign.java
new file mode 100644
index 0000000000..c067c49094
--- /dev/null
+++ b/core/src/main/java/feign/ReflectiveFeign.java
@@ -0,0 +1,394 @@
+/**
+ * Copyright 2012-2020 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;
+ 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 = RequestTemplate.from(metadata.template());
+ mutable.feignTarget(target);
+ if (metadata.urlIndex() != null) {
+ int urlIndex = metadata.urlIndex();
+ checkArgument(argv[urlIndex] != null, "URI parameter %s was null", urlIndex);
+ mutable.target(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()
+ : UriUtils.encode(nextObject.toString()));
+ }
+ } else if (currValue instanceof Object[]) {
+ for (Object value : (Object[]) currValue) {
+ values.add(value == null ? null
+ : encoded ? value.toString() : UriUtils.encode(value.toString()));
+ }
+ } else {
+ values.add(currValue == null ? null
+ : encoded ? currValue.toString() : UriUtils.encode(currValue.toString()));
+ }
+
+ mutable.query(encoded ? currEntry.getKey() : UriUtils.encode(currEntry.getKey()), values);
+ }
+ return mutable;
+ }
+
+ protected RequestTemplate resolve(Object[] argv,
+ RequestTemplate mutable,
+ Map variables) {
+ return mutable.resolve(variables);
+ }
+ }
+
+ private static class BuildFormEncodedTemplateFromArgs extends BuildTemplateByResolvingArgs {
+
+ private final Encoder encoder;
+
+ private BuildFormEncodedTemplateFromArgs(MethodMetadata metadata, Encoder encoder,
+ QueryMapEncoder queryMapEncoder, Target target) {
+ super(metadata, queryMapEncoder, target);
+ 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, Target target) {
+ super(metadata, queryMapEncoder, target);
+ 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..fbf36556d2
--- /dev/null
+++ b/core/src/main/java/feign/Request.java
@@ -0,0 +1,455 @@
+/**
+ * Copyright 2012-2020 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.net.HttpURLConnection;
+import java.nio.charset.Charset;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import static feign.Util.checkNotNull;
+import static feign.Util.valuesOrEmpty;
+
+/**
+ * An immutable request to an http server.
+ */
+public final class Request implements Serializable {
+
+ public enum HttpMethod {
+ GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH
+ }
+
+ /**
+ * No parameters can be null except {@code body} and {@code charset}. All parameters must be
+ * effectively immutable, via safe copies, not mutating or otherwise.
+ *
+ * @deprecated {@link #create(HttpMethod, String, Map, byte[], Charset)}
+ */
+ @Deprecated
+ public static Request create(String method,
+ String url,
+ Map> headers,
+ byte[] body,
+ Charset charset) {
+ checkNotNull(method, "httpMethod of %s", method);
+ final HttpMethod httpMethod = HttpMethod.valueOf(method.toUpperCase());
+ return create(httpMethod, url, headers, body, charset, null);
+ }
+
+ /**
+ * Builds a Request. All parameters must be effectively immutable, via safe copies.
+ *
+ * @param httpMethod for the request.
+ * @param url for the request.
+ * @param headers to include.
+ * @param body of the request, can be {@literal null}
+ * @param charset of the request, can be {@literal null}
+ * @return a Request
+ */
+ @Deprecated
+ public static Request create(HttpMethod httpMethod,
+ String url,
+ Map> headers,
+ byte[] body,
+ Charset charset) {
+ return create(httpMethod, url, headers, Body.create(body, charset), null);
+ }
+
+ /**
+ * Builds a Request. All parameters must be effectively immutable, via safe copies.
+ *
+ * @param httpMethod for the request.
+ * @param url for the request.
+ * @param headers to include.
+ * @param body of the request, can be {@literal null}
+ * @param charset of the request, can be {@literal null}
+ * @return a Request
+ */
+ public static Request create(HttpMethod httpMethod,
+ String url,
+ Map> headers,
+ byte[] body,
+ Charset charset,
+ RequestTemplate requestTemplate) {
+ return create(httpMethod, url, headers, Body.create(body, charset), requestTemplate);
+ }
+
+ /**
+ * Builds a Request. All parameters must be effectively immutable, via safe copies.
+ *
+ * @param httpMethod for the request.
+ * @param url for the request.
+ * @param headers to include.
+ * @param body of the request, can be {@literal null}
+ * @return a Request
+ */
+ public static Request create(HttpMethod httpMethod,
+ String url,
+ Map> headers,
+ Body body,
+ RequestTemplate requestTemplate) {
+ return new Request(httpMethod, url, headers, body, requestTemplate);
+ }
+
+ private final HttpMethod httpMethod;
+ private final String url;
+ private final Map> headers;
+ private final Body body;
+ private final RequestTemplate requestTemplate;
+
+ /**
+ * Creates a new Request.
+ *
+ * @param method of the request.
+ * @param url for the request.
+ * @param headers for the request.
+ * @param body for the request, optional.
+ * @param requestTemplate used to build the request.
+ */
+ Request(HttpMethod method,
+ String url,
+ Map> headers,
+ Body body,
+ RequestTemplate requestTemplate) {
+ this.httpMethod = checkNotNull(method, "httpMethod of %s", method.name());
+ this.url = checkNotNull(url, "url");
+ this.headers = checkNotNull(headers, "headers of %s %s", method, url);
+ this.body = body;
+ this.requestTemplate = requestTemplate;
+ }
+
+ /**
+ * Http Method for this request.
+ *
+ * @return the HttpMethod string
+ * @deprecated @see {@link #httpMethod()}
+ */
+ @Deprecated
+ public String method() {
+ return httpMethod.name();
+ }
+
+ /**
+ * Http Method for the request.
+ *
+ * @return the HttpMethod.
+ */
+ public HttpMethod httpMethod() {
+ return this.httpMethod;
+ }
+
+
+ /**
+ * URL for the request.
+ *
+ * @return URL as a String.
+ */
+ public String url() {
+ return url;
+ }
+
+ /**
+ * Request Headers.
+ *
+ * @return the request headers.
+ */
+ public Map> headers() {
+ return Collections.unmodifiableMap(headers);
+ }
+
+ /**
+ * Charset of the request.
+ *
+ * @return the current character set for the request, may be {@literal null} for binary data.
+ */
+ public Charset charset() {
+ return body.encoding;
+ }
+
+ /**
+ * 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.data;
+ }
+
+ public boolean isBinary() {
+ return body.isBinary();
+ }
+
+ /**
+ * Request Length.
+ *
+ * @return size of the request body.
+ */
+ public int length() {
+ return this.body.length();
+ }
+
+ /**
+ * Request as an HTTP/1.1 request.
+ *
+ * @return the request.
+ */
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append(httpMethod).append(' ').append(url).append(" HTTP/1.1\n");
+ for (final String field : headers.keySet()) {
+ for (final String value : valuesOrEmpty(headers, field)) {
+ builder.append(field).append(": ").append(value).append('\n');
+ }
+ }
+ if (body != null) {
+ builder.append('\n').append(body.asString());
+ }
+ return builder.toString();
+ }
+
+ /**
+ * Controls the per-request settings currently required to be implemented by all {@link Client
+ * clients}
+ */
+ public static class Options {
+
+ private final long connectTimeout;
+ private final TimeUnit connectTimeoutUnit;
+ private final long readTimeout;
+ private final TimeUnit readTimeoutUnit;
+ private final boolean followRedirects;
+
+ /**
+ * Creates a new Options instance.
+ *
+ * @param connectTimeoutMillis connection timeout in milliseconds.
+ * @param readTimeoutMillis read timeout in milliseconds.
+ * @param followRedirects if the request should follow 3xx redirections.
+ *
+ * @deprecated please use {@link #Options(long, TimeUnit, long, TimeUnit, boolean)}
+ */
+ @Deprecated
+ public Options(int connectTimeoutMillis, int readTimeoutMillis, boolean followRedirects) {
+ this(connectTimeoutMillis, TimeUnit.MILLISECONDS,
+ readTimeoutMillis, TimeUnit.MILLISECONDS,
+ followRedirects);
+ }
+
+ /**
+ * Creates a new Options Instance.
+ *
+ * @param connectTimeout value.
+ * @param connectTimeoutUnit with the TimeUnit for the timeout value.
+ * @param readTimeout value.
+ * @param readTimeoutUnit with the TimeUnit for the timeout value.
+ * @param followRedirects if the request should follow 3xx redirections.
+ */
+ public Options(long connectTimeout, TimeUnit connectTimeoutUnit,
+ long readTimeout, TimeUnit readTimeoutUnit,
+ boolean followRedirects) {
+ super();
+ this.connectTimeout = connectTimeout;
+ this.connectTimeoutUnit = connectTimeoutUnit;
+ this.readTimeout = readTimeout;
+ this.readTimeoutUnit = readTimeoutUnit;
+ this.followRedirects = followRedirects;
+ }
+
+ /**
+ * Creates a new Options instance that follows redirects by default.
+ *
+ * @param connectTimeoutMillis connection timeout in milliseconds.
+ * @param readTimeoutMillis read timeout in milliseconds.
+ *
+ * @deprecated please use {@link #Options(long, TimeUnit, long, TimeUnit, boolean)}
+ */
+ @Deprecated
+ public Options(int connectTimeoutMillis, int readTimeoutMillis) {
+ this(connectTimeoutMillis, readTimeoutMillis, true);
+ }
+
+ /**
+ * Creates the new Options instance using the following defaults:
+ *
+ *
Connect Timeout: 10 seconds
+ *
Read Timeout: 60 seconds
+ *
Follow all 3xx redirects
+ *
+ */
+ public Options() {
+ this(10, TimeUnit.SECONDS, 60, TimeUnit.SECONDS, true);
+ }
+
+ /**
+ * Defaults to 10 seconds. {@code 0} implies no timeout.
+ *
+ * @see java.net.HttpURLConnection#getConnectTimeout()
+ */
+ public int connectTimeoutMillis() {
+ return (int) connectTimeoutUnit.toMillis(connectTimeout);
+ }
+
+ /**
+ * Defaults to 60 seconds. {@code 0} implies no timeout.
+ *
+ * @see java.net.HttpURLConnection#getReadTimeout()
+ */
+ public int readTimeoutMillis() {
+ return (int) readTimeoutUnit.toMillis(readTimeout);
+ }
+
+
+ /**
+ * Defaults to true. {@code false} tells the client to not follow the redirections.
+ *
+ * @see HttpURLConnection#getFollowRedirects()
+ */
+ public boolean isFollowRedirects() {
+ return followRedirects;
+ }
+
+ /**
+ * Connect Timeout Value.
+ *
+ * @return current timeout value.
+ */
+ public long connectTimeout() {
+ return connectTimeout;
+ }
+
+ /**
+ * TimeUnit for the Connection Timeout value.
+ *
+ * @return TimeUnit
+ */
+ public TimeUnit connectTimeoutUnit() {
+ return connectTimeoutUnit;
+ }
+
+ /**
+ * Read Timeout value.
+ *
+ * @return current read timeout value.
+ */
+ public long readTimeout() {
+ return readTimeout;
+ }
+
+ /**
+ * TimeUnit for the Read Timeout value.
+ *
+ * @return TimeUnit
+ */
+ public TimeUnit readTimeoutUnit() {
+ return readTimeoutUnit;
+ }
+
+ }
+
+ @Experimental
+ public RequestTemplate requestTemplate() {
+ return this.requestTemplate;
+ }
+
+ /**
+ * Request Body
+ *
+ * Considered experimental, will most likely be made internal going forward.
+ *
+ */
+ @Experimental
+ public static class Body implements Serializable {
+
+ private transient Charset encoding;
+
+ private byte[] data;
+
+ private Body() {
+ super();
+ }
+
+ private Body(byte[] data) {
+ this.data = data;
+ }
+
+ private Body(byte[] data, Charset encoding) {
+ this.data = data;
+ this.encoding = encoding;
+ }
+
+ public Optional getEncoding() {
+ return Optional.ofNullable(this.encoding);
+ }
+
+ public int length() {
+ /* calculate the content length based on the data provided */
+ return data != null ? data.length : 0;
+ }
+
+ public byte[] asBytes() {
+ return data;
+ }
+
+ public String asString() {
+ return !isBinary()
+ ? new String(data, encoding)
+ : "Binary data";
+ }
+
+ public boolean isBinary() {
+ return encoding == null || data == null;
+ }
+
+ public static Body create(String data) {
+ return new Body(data.getBytes());
+ }
+
+ public static Body create(String data, Charset charset) {
+ return new Body(data.getBytes(charset), charset);
+ }
+
+ public static Body create(byte[] data) {
+ return new Body(data);
+ }
+
+ public static Body create(byte[] data, Charset charset) {
+ return new Body(data, charset);
+ }
+
+ /**
+ * Creates a new Request Body with charset encoded data.
+ *
+ * @param data to be encoded.
+ * @param charset to encode the data with. if {@literal null}, then data will be considered
+ * binary and will not be encoded.
+ *
+ * @return a new Request.Body instance with the encoded data.
+ * @deprecated please use {@link Request.Body#create(byte[], Charset)}
+ */
+ @Deprecated
+ public static Body encoded(byte[] data, Charset charset) {
+ return create(data, charset);
+ }
+
+ public static Body empty() {
+ return new Body();
+ }
+
+ }
+}
diff --git a/core/src/main/java/feign/RequestInterceptor.java b/core/src/main/java/feign/RequestInterceptor.java
new file mode 100644
index 0000000000..43a10fdadc
--- /dev/null
+++ b/core/src/main/java/feign/RequestInterceptor.java
@@ -0,0 +1,56 @@
+/**
+ * Copyright 2012-2020 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 given with regards to the order that interceptors are applied.
+ * Once interceptors are applied, {@link Target#apply(RequestTemplate)} is called to create the
+ * immutable http request sent via {@link Client#execute(Request, feign.Request.Options)}.
+ *
+ * For example:
+ *
+ *
+ *
+ *
+ *
+ * Configuration
+ *
+ * {@code RequestInterceptors} are configured via {@link Feign.Builder#requestInterceptors}.
+ *
+ * Implementation notes
+ *
+ * Do not add parameters, such as {@code /path/{foo}/bar } in your implementation of
+ * {@link #apply(RequestTemplate)}.
+ * Interceptors are applied after the template's parameters are
+ * {@link RequestTemplate#resolve(java.util.Map) resolved}. This is to ensure that you can implement
+ * signatures are interceptors.
+ *
+ *
+ * Relationship to Retrofit 1.x
+ *
+ * This class is similar to {@code RequestInterceptor.intercept()}, except that the implementation
+ * can read, remove, or otherwise mutate any part of the request template.
+ */
+public interface RequestInterceptor {
+
+ /**
+ * Called for every request. Add data using methods on the supplied {@link RequestTemplate}.
+ */
+ void apply(RequestTemplate template);
+}
diff --git a/core/src/main/java/feign/RequestLine.java b/core/src/main/java/feign/RequestLine.java
new file mode 100644
index 0000000000..aaffe07fae
--- /dev/null
+++ b/core/src/main/java/feign/RequestLine.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright 2012-2020 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 uri template supplied in the {@code value}, permitting path and query variables, or
+ * just the http method. Templates should conform to
+ * RFC 6570. Support is limited to Simple String
+ * expansion and Reserved Expansion (Level 1 and Level 2) expressions.
+ */
+@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..0892cc6f2e
--- /dev/null
+++ b/core/src/main/java/feign/RequestTemplate.java
@@ -0,0 +1,1033 @@
+/**
+ * Copyright 2012-2020 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.Request.HttpMethod;
+import feign.template.*;
+import java.io.Serializable;
+import java.net.URI;
+import java.nio.charset.Charset;
+import java.util.AbstractMap.SimpleImmutableEntry;
+import java.util.*;
+import java.util.Map.Entry;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import static feign.Util.*;
+
+/**
+ * Request Builder for an HTTP Target.
+ *
+ * This class is a variation on a UriTemplate, where, in addition to the uri, Headers and Query
+ * information also support template expressions.
+ *
+ */
+@SuppressWarnings("UnusedReturnValue")
+public final class RequestTemplate implements Serializable {
+
+ private static final Pattern QUERY_STRING_PATTERN = Pattern.compile("(? queries = new LinkedHashMap<>();
+ private final Map headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ private String target;
+ private String fragment;
+ private boolean resolved = false;
+ private UriTemplate uriTemplate;
+ private BodyTemplate bodyTemplate;
+ private HttpMethod method;
+ private transient Charset charset = Util.UTF_8;
+ private Request.Body body = Request.Body.empty();
+ private boolean decodeSlash = true;
+ private CollectionFormat collectionFormat = CollectionFormat.EXPLODED;
+ private MethodMetadata methodMetadata;
+ private Target> feignTarget;
+
+ /**
+ * Create a new Request Template.
+ */
+ public RequestTemplate() {
+ super();
+ }
+
+ /**
+ * Create a new Request Template.
+ *
+ * @param fragment part of the request uri.
+ * @param target for the template.
+ * @param uriTemplate for the template.
+ * @param bodyTemplate for the template, may be {@literal null}
+ * @param method of the request.
+ * @param charset for the request.
+ * @param body of the request, may be {@literal null}
+ * @param decodeSlash if the request uri should encode slash characters.
+ * @param collectionFormat when expanding collection based variables.
+ * @param feignTarget this template is targeted for.
+ * @param methodMetadata containing a reference to the method this template is built from.
+ */
+ private RequestTemplate(String target,
+ String fragment,
+ UriTemplate uriTemplate,
+ BodyTemplate bodyTemplate,
+ HttpMethod method,
+ Charset charset,
+ Request.Body body,
+ boolean decodeSlash,
+ CollectionFormat collectionFormat,
+ MethodMetadata methodMetadata,
+ Target> feignTarget) {
+ this.target = target;
+ this.fragment = fragment;
+ this.uriTemplate = uriTemplate;
+ this.bodyTemplate = bodyTemplate;
+ this.method = method;
+ this.charset = charset;
+ this.body = body;
+ this.decodeSlash = decodeSlash;
+ this.collectionFormat =
+ (collectionFormat != null) ? collectionFormat : CollectionFormat.EXPLODED;
+ this.methodMetadata = methodMetadata;
+ this.feignTarget = feignTarget;
+ }
+
+ /**
+ * Create a Request Template from an existing Request Template.
+ *
+ * @param requestTemplate to copy from.
+ * @return a new Request Template.
+ */
+ public static RequestTemplate from(RequestTemplate requestTemplate) {
+ RequestTemplate template =
+ new RequestTemplate(
+ requestTemplate.target,
+ requestTemplate.fragment,
+ requestTemplate.uriTemplate,
+ requestTemplate.bodyTemplate,
+ requestTemplate.method,
+ requestTemplate.charset,
+ requestTemplate.body,
+ requestTemplate.decodeSlash,
+ requestTemplate.collectionFormat,
+ requestTemplate.methodMetadata,
+ requestTemplate.feignTarget);
+
+ if (!requestTemplate.queries().isEmpty()) {
+ template.queries.putAll(requestTemplate.queries);
+ }
+
+ if (!requestTemplate.headers().isEmpty()) {
+ template.headers.putAll(requestTemplate.headers);
+ }
+ return template;
+ }
+
+ /**
+ * Create a Request Template from an existing Request Template.
+ *
+ * @param toCopy template.
+ * @deprecated replaced by {@link RequestTemplate#from(RequestTemplate)}
+ */
+ @Deprecated
+ public RequestTemplate(RequestTemplate toCopy) {
+ checkNotNull(toCopy, "toCopy");
+ this.target = toCopy.target;
+ this.fragment = toCopy.fragment;
+ this.method = toCopy.method;
+ this.queries.putAll(toCopy.queries);
+ this.headers.putAll(toCopy.headers);
+ this.charset = toCopy.charset;
+ this.body = toCopy.body;
+ this.decodeSlash = toCopy.decodeSlash;
+ this.collectionFormat =
+ (toCopy.collectionFormat != null) ? toCopy.collectionFormat : CollectionFormat.EXPLODED;
+ this.uriTemplate = toCopy.uriTemplate;
+ this.bodyTemplate = toCopy.bodyTemplate;
+ this.resolved = false;
+ this.methodMetadata = toCopy.methodMetadata;
+ this.target = toCopy.target;
+ this.feignTarget = toCopy.feignTarget;
+ }
+
+ /**
+ * Resolve all expressions using the variable value substitutions provided. Variable values will
+ * be pct-encoded, if they are not already.
+ *
+ * @param variables containing the variable values to use when resolving expressions.
+ * @return a new Request Template with all of the variables resolved.
+ */
+ public RequestTemplate resolve(Map variables) {
+
+ StringBuilder uri = new StringBuilder();
+
+ /* create a new template form this one, but explicitly */
+ RequestTemplate resolved = RequestTemplate.from(this);
+
+ if (this.uriTemplate == null) {
+ /* create a new uri template using the default root */
+ this.uriTemplate = UriTemplate.create("", !this.decodeSlash, this.charset);
+ }
+
+ String expanded = this.uriTemplate.expand(variables);
+ if (expanded != null) {
+ uri.append(expanded);
+ }
+
+ /*
+ * for simplicity, combine the queries into the uri and use the resulting uri to seed the
+ * resolved template.
+ */
+ if (!this.queries.isEmpty()) {
+ /*
+ * since we only want to keep resolved query values, reset any queries on the resolved copy
+ */
+ resolved.queries(Collections.emptyMap());
+ StringBuilder query = new StringBuilder();
+ Iterator queryTemplates = this.queries.values().iterator();
+
+ while (queryTemplates.hasNext()) {
+ QueryTemplate queryTemplate = queryTemplates.next();
+ String queryExpanded = queryTemplate.expand(variables);
+ if (Util.isNotBlank(queryExpanded)) {
+ query.append(queryExpanded);
+ if (queryTemplates.hasNext()) {
+ query.append("&");
+ }
+ }
+ }
+
+ String queryString = query.toString();
+ if (!queryString.isEmpty()) {
+ Matcher queryMatcher = QUERY_STRING_PATTERN.matcher(uri);
+ if (queryMatcher.find()) {
+ /* the uri already has a query, so any additional queries should be appended */
+ uri.append("&");
+ } else {
+ uri.append("?");
+ }
+ uri.append(queryString);
+ }
+ }
+
+ /* add the uri to result */
+ resolved.uri(uri.toString());
+
+ /* headers */
+ if (!this.headers.isEmpty()) {
+ /*
+ * same as the query string, we only want to keep resolved values, so clear the header map on
+ * the resolved instance
+ */
+ resolved.headers(Collections.emptyMap());
+ for (HeaderTemplate headerTemplate : this.headers.values()) {
+ /* resolve the header */
+ String header = headerTemplate.expand(variables);
+ if (!header.isEmpty()) {
+ /* append the header as a new literal as the value has already been expanded. */
+ resolved.header(headerTemplate.getName(), header);
+ }
+ }
+ }
+
+ if (this.bodyTemplate != null) {
+ resolved.body(this.bodyTemplate.expand(variables));
+ }
+
+ /* mark the new template resolved */
+ resolved.resolved = true;
+ return resolved;
+ }
+
+ /**
+ * Resolves all expressions, using the variables provided. Values not present in the {@code
+ * alreadyEncoded} map are pct-encoded.
+ *
+ * @param unencoded variable values to substitute.
+ * @param alreadyEncoded variable names.
+ * @return a resolved Request Template
+ * @deprecated use {@link RequestTemplate#resolve(Map)}. Values already encoded are recognized as
+ * such and skipped.
+ */
+ @SuppressWarnings("unused")
+ @Deprecated
+ RequestTemplate resolve(Map unencoded, Map alreadyEncoded) {
+ return this.resolve(unencoded);
+ }
+
+ /**
+ * Creates a {@link Request} from this template. The template must be resolved before calling this
+ * method, or an {@link IllegalStateException} will be thrown.
+ *
+ * @return a new Request instance.
+ * @throws IllegalStateException if this template has not been resolved.
+ */
+ public Request request() {
+ if (!this.resolved) {
+ throw new IllegalStateException("template has not been resolved.");
+ }
+ return Request.create(this.method, this.url(), this.headers(), this.body, this);
+ }
+
+ /**
+ * Set the Http Method.
+ *
+ * @param method to use.
+ * @return a RequestTemplate for chaining.
+ * @deprecated see {@link RequestTemplate#method(HttpMethod)}
+ */
+ @Deprecated
+ public RequestTemplate method(String method) {
+ checkNotNull(method, "method");
+ try {
+ this.method = HttpMethod.valueOf(method);
+ } catch (IllegalArgumentException iae) {
+ throw new IllegalArgumentException("Invalid HTTP Method: " + method);
+ }
+ return this;
+ }
+
+ /**
+ * Set the Http Method.
+ *
+ * @param method to use.
+ * @return a RequestTemplate for chaining.
+ */
+ public RequestTemplate method(HttpMethod method) {
+ checkNotNull(method, "method");
+ this.method = method;
+ return this;
+ }
+
+ /**
+ * The Request Http Method.
+ *
+ * @return Http Method.
+ */
+ public String method() {
+ return (method != null) ? method.name() : null;
+ }
+
+ /**
+ * Set whether do encode slash {@literal /} characters when resolving this template.
+ *
+ * @param decodeSlash if slash literals should not be encoded.
+ * @return a RequestTemplate for chaining.
+ */
+ public RequestTemplate decodeSlash(boolean decodeSlash) {
+ this.decodeSlash = decodeSlash;
+ this.uriTemplate =
+ UriTemplate.create(this.uriTemplate.toString(), !this.decodeSlash, this.charset);
+ if (!this.queries.isEmpty()) {
+ this.queries.replaceAll((key, queryTemplate) -> QueryTemplate.create(
+ /* replace the current template with new ones honoring the decode value */
+ queryTemplate.getName(), queryTemplate.getValues(), charset, collectionFormat,
+ decodeSlash));
+
+ }
+ return this;
+ }
+
+ /**
+ * If slash {@literal /} characters are not encoded when resolving.
+ *
+ * @return true if slash literals are not encoded, false otherwise.
+ */
+ public boolean decodeSlash() {
+ return decodeSlash;
+ }
+
+ /**
+ * The Collection Format to use when resolving variables that represent {@link Iterable}s or
+ * {@link Collection}s
+ *
+ * @param collectionFormat to use.
+ * @return a RequestTemplate for chaining.
+ */
+ public RequestTemplate collectionFormat(CollectionFormat collectionFormat) {
+ this.collectionFormat = collectionFormat;
+ return this;
+ }
+
+ /**
+ * The Collection Format that will be used when resolving {@link Iterable} and {@link Collection}
+ * variables.
+ *
+ * @return the collection format set
+ */
+ @SuppressWarnings("unused")
+ public CollectionFormat collectionFormat() {
+ return collectionFormat;
+ }
+
+ /**
+ * Append the value to the template.
+ *
+ * This method is poorly named and is used primarily to store the relative uri for the request. It
+ * has been replaced by {@link RequestTemplate#uri(String)} and will be removed in a future
+ * release.
+ *
+ *
+ * @param value to append.
+ * @return a RequestTemplate for chaining.
+ * @deprecated see {@link RequestTemplate#uri(String, boolean)}
+ */
+ @Deprecated
+ public RequestTemplate append(CharSequence value) {
+ /* proxy to url */
+ if (this.uriTemplate != null) {
+ return this.uri(value.toString(), true);
+ }
+ return this.uri(value.toString());
+ }
+
+ /**
+ * Insert the value at the specified point in the template uri.
+ *
+ * This method is poorly named has undocumented behavior. When the value contains a fully
+ * qualified http request url, the value is always inserted at the beginning of the uri.
+ *
+ *
+ * Due to this, use of this method is not recommended and remains for backward compatibility. It
+ * has been replaced by {@link RequestTemplate#target(String)} and will be removed in a future
+ * release.
+ *
+ *
+ * @param pos in the uri to place the value.
+ * @param value to insert.
+ * @return a RequestTemplate for chaining.
+ * @deprecated see {@link RequestTemplate#target(String)}
+ */
+ @SuppressWarnings("unused")
+ @Deprecated
+ public RequestTemplate insert(int pos, CharSequence value) {
+ return target(value.toString());
+ }
+
+ /**
+ * Set the Uri for the request, replacing the existing uri if set.
+ *
+ * @param uri to use, must be a relative uri.
+ * @return a RequestTemplate for chaining.
+ */
+ public RequestTemplate uri(String uri) {
+ return this.uri(uri, false);
+ }
+
+ /**
+ * Set the uri for the request.
+ *
+ * @param uri to use, must be a relative uri.
+ * @param append if the uri should be appended, if the uri is already set.
+ * @return a RequestTemplate for chaining.
+ */
+ public RequestTemplate uri(String uri, boolean append) {
+ /* validate and ensure that the url is always a relative one */
+ if (UriUtils.isAbsolute(uri)) {
+ throw new IllegalArgumentException("url values must be not be absolute.");
+ }
+
+ if (uri == null) {
+ uri = "/";
+ } else if ((!uri.isEmpty() && !uri.startsWith("/") && !uri.startsWith("{")
+ && !uri.startsWith("?") && !uri.startsWith(";"))) {
+ /* if the start of the url is a literal, it must begin with a slash. */
+ uri = "/" + uri;
+ }
+
+ /*
+ * templates may provide query parameters. since we want to manage those explicity, we will need
+ * to extract those out, leaving the uriTemplate with only the path to deal with.
+ */
+ Matcher queryMatcher = QUERY_STRING_PATTERN.matcher(uri);
+ if (queryMatcher.find()) {
+ String queryString = uri.substring(queryMatcher.start() + 1);
+
+ /* parse the query string */
+ this.extractQueryTemplates(queryString, append);
+
+ /* reduce the uri to the path */
+ uri = uri.substring(0, queryMatcher.start());
+ }
+
+ int fragmentIndex = uri.indexOf('#');
+ if (fragmentIndex > -1) {
+ fragment = uri.substring(fragmentIndex);
+ uri = uri.substring(0, fragmentIndex);
+ }
+
+ /* replace the uri template */
+ if (append && this.uriTemplate != null) {
+ this.uriTemplate = UriTemplate.append(this.uriTemplate, uri);
+ } else {
+ this.uriTemplate = UriTemplate.create(uri, !this.decodeSlash, this.charset);
+ }
+ return this;
+ }
+
+ /**
+ * Set the target host for this request.
+ *
+ * @param target host for this request. Must be an absolute target.
+ * @return a RequestTemplate for chaining.
+ */
+ public RequestTemplate target(String target) {
+ /* target can be empty */
+ if (Util.isBlank(target)) {
+ return this;
+ }
+
+ /* verify that the target contains the scheme, host and port */
+ if (!UriUtils.isAbsolute(target)) {
+ throw new IllegalArgumentException("target values must be absolute.");
+ }
+ if (target.endsWith("/")) {
+ target = target.substring(0, target.length() - 1);
+ }
+ try {
+ /* parse the target */
+ URI targetUri = URI.create(target);
+
+ if (Util.isNotBlank(targetUri.getRawQuery())) {
+ /*
+ * target has a query string, we need to make sure that they are recorded as queries
+ */
+ this.extractQueryTemplates(targetUri.getRawQuery(), true);
+ }
+
+ /* strip the query string */
+ this.target = targetUri.getScheme() + "://" + targetUri.getAuthority() + targetUri.getPath();
+ if (targetUri.getFragment() != null) {
+ this.fragment = "#" + targetUri.getFragment();
+ }
+ } catch (IllegalArgumentException iae) {
+ /* the uri provided is not a valid one, we can't continue */
+ throw new IllegalArgumentException("Target is not a valid URI.", iae);
+ }
+ return this;
+ }
+
+ /**
+ * The URL for the request. If the template has not been resolved, the url will represent a uri
+ * template.
+ *
+ * @return the url
+ */
+ public String url() {
+
+ /* build the fully qualified url with all query parameters */
+ StringBuilder url = new StringBuilder(this.path());
+ if (!this.queries.isEmpty()) {
+ url.append(this.queryLine());
+ }
+ if (fragment != null) {
+ url.append(fragment);
+ }
+
+ return url.toString();
+ }
+
+ /**
+ * The Uri Path.
+ *
+ * @return the uri path.
+ */
+ public String path() {
+ /* build the fully qualified url with all query parameters */
+ StringBuilder path = new StringBuilder();
+ if (this.target != null) {
+ path.append(this.target);
+ }
+ if (this.uriTemplate != null) {
+ path.append(this.uriTemplate.toString());
+ }
+ if (path.length() == 0) {
+ /* no path indicates the root uri */
+ path.append("/");
+ }
+ return path.toString();
+
+ }
+
+ /**
+ * List all of the template variable expressions for this template.
+ *
+ * @return a list of template variable names
+ */
+ public List variables() {
+ /* combine the variables from the uri, query, header, and body templates */
+ List variables = new ArrayList<>(this.uriTemplate.getVariables());
+
+ /* queries */
+ for (QueryTemplate queryTemplate : this.queries.values()) {
+ variables.addAll(queryTemplate.getVariables());
+ }
+
+ /* headers */
+ for (HeaderTemplate headerTemplate : this.headers.values()) {
+ variables.addAll(headerTemplate.getVariables());
+ }
+
+ /* body */
+ if (this.bodyTemplate != null) {
+ variables.addAll(this.bodyTemplate.getVariables());
+ }
+
+ return variables;
+ }
+
+ /**
+ * @see RequestTemplate#query(String, Iterable)
+ */
+ public RequestTemplate query(String name, String... values) {
+ if (values == null) {
+ return query(name, Collections.emptyList());
+ }
+ return query(name, Arrays.asList(values));
+ }
+
+
+ /**
+ * Specify a Query String parameter, with the specified values. Values can be literals or template
+ * expressions.
+ *
+ * @param name of the parameter.
+ * @param values for this parameter.
+ * @return a RequestTemplate for chaining.
+ */
+ public RequestTemplate query(String name, Iterable values) {
+ return appendQuery(name, values, this.collectionFormat);
+ }
+
+ /**
+ * Specify a Query String parameter, with the specified values. Values can be literals or template
+ * expressions.
+ *
+ * @param name of the parameter.
+ * @param values for this parameter.
+ * @param collectionFormat to use when resolving collection based expressions.
+ * @return a Request Template for chaining.
+ */
+ public RequestTemplate query(String name,
+ Iterable values,
+ CollectionFormat collectionFormat) {
+ return appendQuery(name, values, collectionFormat);
+ }
+
+ /**
+ * Appends the query name and values.
+ *
+ * @param name of the parameter.
+ * @param values for the parameter, may be expressions.
+ * @param collectionFormat to use when resolving collection based query variables.
+ * @return a RequestTemplate for chaining.
+ */
+ private RequestTemplate appendQuery(String name,
+ Iterable values,
+ CollectionFormat collectionFormat) {
+ if (!values.iterator().hasNext()) {
+ /* empty value, clear the existing values */
+ this.queries.remove(name);
+ return this;
+ }
+
+ /* create a new query template out of the information here */
+ this.queries.compute(name, (key, queryTemplate) -> {
+ if (queryTemplate == null) {
+ return QueryTemplate.create(name, values, this.charset, collectionFormat, this.decodeSlash);
+ } else {
+ return QueryTemplate.append(queryTemplate, values, collectionFormat, this.decodeSlash);
+ }
+ });
+ return this;
+ }
+
+ /**
+ * Sets the Query Parameters.
+ *
+ * @param queries to use for this request.
+ * @return a RequestTemplate for chaining.
+ */
+ @SuppressWarnings("unused")
+ public RequestTemplate queries(Map> queries) {
+ if (queries == null || queries.isEmpty()) {
+ this.queries.clear();
+ } else {
+ queries.forEach(this::query);
+ }
+ return this;
+ }
+
+
+ /**
+ * Return an immutable Map of all Query Parameters and their values.
+ *
+ * @return registered Query Parameters.
+ */
+ public Map> queries() {
+ Map> queryMap = new LinkedHashMap<>();
+ this.queries.forEach((key, queryTemplate) -> {
+ List values = new ArrayList<>(queryTemplate.getValues());
+
+ /* add the expanded collection, but lock it */
+ queryMap.put(key, Collections.unmodifiableList(values));
+ });
+
+ return Collections.unmodifiableMap(queryMap);
+ }
+
+ /**
+ * @see RequestTemplate#header(String, Iterable)
+ */
+ public RequestTemplate header(String name, String... values) {
+ return header(name, Arrays.asList(values));
+ }
+
+ /**
+ * Specify a Header, with the specified values. Values can be literals or template expressions.
+ *
+ * @param name of the header.
+ * @param values for this header.
+ * @return a RequestTemplate for chaining.
+ */
+ public RequestTemplate header(String name, Iterable values) {
+ if (name == null || name.isEmpty()) {
+ throw new IllegalArgumentException("name is required.");
+ }
+ if (values == null) {
+ values = Collections.emptyList();
+ }
+
+ return appendHeader(name, values);
+ }
+
+ /**
+ * Clear on reader from {@link RequestTemplate}
+ *
+ * @param name of the header.
+ * @return a RequestTemplate for chaining.
+ */
+ public RequestTemplate removeHeader(String name) {
+ if (name == null || name.isEmpty()) {
+ throw new IllegalArgumentException("name is required.");
+ }
+ this.headers.remove(name);
+ return this;
+ }
+
+ /**
+ * Create a Header Template.
+ *
+ * @param name of the header
+ * @param values for the header, may be expressions.
+ * @return a RequestTemplate for chaining.
+ */
+ private RequestTemplate appendHeader(String name, Iterable values) {
+ if (!values.iterator().hasNext()) {
+ /* empty value, clear the existing values */
+ this.headers.remove(name);
+ return this;
+ }
+ if (name.equals("Content-Type")) {
+ // a client can only produce content of one single type, so always override Content-Type and
+ // only add a single type
+ this.headers.remove(name);
+ this.headers.put(name,
+ HeaderTemplate.create(name, Collections.singletonList(values.iterator().next())));
+ return this;
+ }
+ this.headers.compute(name, (headerName, headerTemplate) -> {
+ if (headerTemplate == null) {
+ return HeaderTemplate.create(headerName, values);
+ } else {
+ return HeaderTemplate.append(headerTemplate, values);
+ }
+ });
+ return this;
+ }
+
+ /**
+ * Headers for this Request.
+ *
+ * @param headers to use.
+ * @return a RequestTemplate for chaining.
+ */
+ public RequestTemplate headers(Map> headers) {
+ if (headers != null && !headers.isEmpty()) {
+ headers.forEach(this::header);
+ } else {
+ this.headers.clear();
+ }
+ return this;
+ }
+
+ /**
+ * Returns an immutable copy of the Headers for this request.
+ *
+ * @return the currently applied headers.
+ */
+ public Map> headers() {
+ Map> headerMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ this.headers.forEach((key, headerTemplate) -> {
+ List values = new ArrayList<>(headerTemplate.getValues());
+
+ /* add the expanded collection, but only if it has values */
+ if (!values.isEmpty()) {
+ headerMap.put(key, Collections.unmodifiableList(values));
+ }
+ });
+ return Collections.unmodifiableMap(headerMap);
+ }
+
+ /**
+ * Sets the Body and Charset for this request.
+ *
+ * @param data to send, can be null.
+ * @param charset of the encoded data.
+ * @return a RequestTemplate for chaining.
+ */
+ public RequestTemplate body(byte[] data, Charset charset) {
+ this.body(Request.Body.create(data, charset));
+ return this;
+ }
+
+ /**
+ * Set the Body for this request. Charset is assumed to be UTF_8. Data must be encoded.
+ *
+ * @param bodyText to send.
+ * @return a RequestTemplate for chaining.
+ */
+ public RequestTemplate body(String bodyText) {
+ this.body(Request.Body.create(bodyText.getBytes(this.charset), this.charset));
+ return this;
+ }
+
+ /**
+ * Set the Body for this request.
+ *
+ * @param body to send.
+ * @return a RequestTemplate for chaining.
+ * @deprecated use {@link #body(byte[], Charset)} instead.
+ */
+ @Deprecated
+ public RequestTemplate body(Request.Body body) {
+ this.body = body;
+
+ /* body template must be cleared to prevent double processing */
+ this.bodyTemplate = null;
+
+ header(CONTENT_LENGTH, Collections.emptyList());
+ if (body.length() > 0) {
+ header(CONTENT_LENGTH, String.valueOf(body.length()));
+ }
+
+ return this;
+ }
+
+ /**
+ * Charset of the Request Body, if known.
+ *
+ * @return the currently applied Charset.
+ */
+ public Charset requestCharset() {
+ if (this.body != null) {
+ return this.body.getEncoding()
+ .orElse(this.charset);
+ }
+ return this.charset;
+ }
+
+ /**
+ * The Request Body.
+ *
+ * @return the request body.
+ */
+ public byte[] body() {
+ return body.asBytes();
+ }
+
+ /**
+ * The Request.Body internal object.
+ *
+ * @return the internal Request.Body.
+ * @deprecated this abstraction is leaky and will be removed in later releases.
+ */
+ @Deprecated
+ public Request.Body requestBody() {
+ return this.body;
+ }
+
+
+ /**
+ * Specify the Body Template to use. Can contain literals and expressions.
+ *
+ * @param bodyTemplate to use.
+ * @return a RequestTemplate for chaining.
+ */
+ public RequestTemplate bodyTemplate(String bodyTemplate) {
+ this.bodyTemplate = BodyTemplate.create(bodyTemplate, this.charset);
+ return this;
+ }
+
+ /**
+ * Specify the Body Template to use. Can contain literals and expressions.
+ *
+ * @param bodyTemplate to use.
+ * @return a RequestTemplate for chaining.
+ */
+ public RequestTemplate bodyTemplate(String bodyTemplate, Charset charset) {
+ this.bodyTemplate = BodyTemplate.create(bodyTemplate, charset);
+ this.charset = charset;
+ return this;
+ }
+
+ /**
+ * Body Template to resolve.
+ *
+ * @return the unresolved body template.
+ */
+ public String bodyTemplate() {
+ if (this.bodyTemplate != null) {
+ return this.bodyTemplate.toString();
+ }
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ return request().toString();
+ }
+
+ /**
+ * Return if the variable exists on the uri, query, or headers, in this template.
+ *
+ * @param variable to look for.
+ * @return true if the variable exists, false otherwise.
+ */
+ public boolean hasRequestVariable(String variable) {
+ return this.getRequestVariables().contains(variable);
+ }
+
+ /**
+ * Retrieve all uri, header, and query template variables.
+ *
+ * @return a List of all the variable names.
+ */
+ public Collection getRequestVariables() {
+ final Collection variables = new LinkedHashSet<>(this.uriTemplate.getVariables());
+ this.queries.values().forEach(queryTemplate -> variables.addAll(queryTemplate.getVariables()));
+ this.headers.values()
+ .forEach(headerTemplate -> variables.addAll(headerTemplate.getVariables()));
+ return variables;
+ }
+
+ /**
+ * If this template has been resolved.
+ *
+ * @return true if the template has been resolved, false otherwise.
+ */
+ @SuppressWarnings("unused")
+ public boolean resolved() {
+ return this.resolved;
+ }
+
+ /**
+ * The Query String for the template. Expressions are not resolved.
+ *
+ * @return the Query String.
+ */
+ public String queryLine() {
+ StringBuilder queryString = new StringBuilder();
+
+ if (!this.queries.isEmpty()) {
+ Iterator iterator = this.queries.values().iterator();
+ while (iterator.hasNext()) {
+ QueryTemplate queryTemplate = iterator.next();
+ String query = queryTemplate.toString();
+ if (query != null && !query.isEmpty()) {
+ queryString.append(query);
+ if (iterator.hasNext()) {
+ queryString.append("&");
+ }
+ }
+ }
+ }
+ /* remove any trailing ampersands */
+ String result = queryString.toString();
+ if (result.endsWith("&")) {
+ result = result.substring(0, result.length() - 1);
+ }
+
+ if (!result.isEmpty()) {
+ result = "?" + result;
+ }
+
+ return result;
+ }
+
+ private void extractQueryTemplates(String queryString, boolean append) {
+ /* split the query string up into name value pairs */
+ Map> queryParameters =
+ Arrays.stream(queryString.split("&"))
+ .map(this::splitQueryParameter)
+ .collect(Collectors.groupingBy(
+ SimpleImmutableEntry::getKey,
+ LinkedHashMap::new,
+ Collectors.mapping(Entry::getValue, Collectors.toList())));
+
+ /* add them to this template */
+ if (!append) {
+ /* clear the queries and use the new ones */
+ this.queries.clear();
+ }
+ queryParameters.forEach(this::query);
+ }
+
+ private SimpleImmutableEntry splitQueryParameter(String pair) {
+ int eq = pair.indexOf("=");
+ final String name = (eq > 0) ? pair.substring(0, eq) : pair;
+ final String value = (eq > 0 && eq < pair.length()) ? pair.substring(eq + 1) : null;
+ return new SimpleImmutableEntry<>(name, value);
+ }
+
+ @Experimental
+ public RequestTemplate methodMetadata(MethodMetadata methodMetadata) {
+ this.methodMetadata = methodMetadata;
+ return this;
+ }
+
+ @Experimental
+ public RequestTemplate feignTarget(Target> feignTarget) {
+ this.feignTarget = feignTarget;
+ return this;
+ }
+
+ @Experimental
+ public MethodMetadata methodMetadata() {
+ return methodMetadata;
+ }
+
+ @Experimental
+ public Target> feignTarget() {
+ return feignTarget;
+ }
+
+ /**
+ * Factory for creating RequestTemplate.
+ */
+ interface Factory {
+
+ /**
+ * create a request template using args passed to a method invocation.
+ */
+ RequestTemplate create(Object[] argv);
+ }
+
+}
diff --git a/core/src/main/java/feign/Response.java b/core/src/main/java/feign/Response.java
new file mode 100644
index 0000000000..4479b18488
--- /dev/null
+++ b/core/src/main/java/feign/Response.java
@@ -0,0 +1,376 @@
+/**
+ * Copyright 2012-2021 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.*;
+import java.io.*;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+
+/**
+ * An immutable response to an http invocation which only returns string content.
+ */
+public final class Response implements Closeable {
+
+ private final int status;
+ private final String reason;
+ private final Map> headers;
+ private final Body body;
+ private final Request request;
+
+ private Response(Builder builder) {
+ checkState(builder.request != null, "original request is required");
+ this.status = builder.status;
+ this.request = builder.request;
+ this.reason = builder.reason; // nullable
+ this.headers = (builder.headers != null)
+ ? Collections.unmodifiableMap(caseInsensitiveCopyOf(builder.headers))
+ : new LinkedHashMap<>();
+ this.body = builder.body; // nullable
+
+ }
+
+ public Builder toBuilder() {
+ return new Builder(this);
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static final class Builder {
+ int status;
+ String reason;
+ Map> headers;
+ Body body;
+ Request request;
+ private RequestTemplate requestTemplate;
+
+ Builder() {}
+
+ Builder(Response source) {
+ this.status = source.status;
+ this.reason = source.reason;
+ this.headers = source.headers;
+ this.body = source.body;
+ this.request = source.request;
+ }
+
+ /** @see Response#status */
+ public Builder status(int status) {
+ this.status = status;
+ return this;
+ }
+
+ /** @see Response#reason */
+ public Builder reason(String reason) {
+ this.reason = reason;
+ return this;
+ }
+
+ /** @see Response#headers */
+ public Builder headers(Map> headers) {
+ this.headers = headers;
+ return this;
+ }
+
+ /** @see Response#body */
+ public Builder body(Body body) {
+ this.body = body;
+ return this;
+ }
+
+ /** @see Response#body */
+ public Builder body(InputStream inputStream, Integer length) {
+ this.body = InputStreamBody.orNull(inputStream, length);
+ return this;
+ }
+
+ /** @see Response#body */
+ public Builder body(byte[] data) {
+ this.body = ByteArrayBody.orNull(data);
+ return this;
+ }
+
+ /** @see Response#body */
+ public Builder body(String text, Charset charset) {
+ this.body = ByteArrayBody.orNull(text, charset);
+ return this;
+ }
+
+ /**
+ * @see Response#request
+ */
+ public Builder request(Request request) {
+ checkNotNull(request, "request is required");
+ this.request = request;
+ return this;
+ }
+
+ /**
+ * The Request Template used for the original request.
+ *
+ * @param requestTemplate used.
+ * @return builder reference.
+ */
+ @Experimental
+ public Builder requestTemplate(RequestTemplate requestTemplate) {
+ this.requestTemplate = requestTemplate;
+ return this;
+ }
+
+ public Response build() {
+ return new Response(this);
+ }
+ }
+
+ /**
+ * status code. ex {@code 200}
+ *
+ * See rfc2616
+ */
+ public int status() {
+ return status;
+ }
+
+ /**
+ * Nullable and not set when using http/2
+ *
+ * See https://github.com/http2/http2-spec/issues/202
+ */
+ public String reason() {
+ return reason;
+ }
+
+ /**
+ * Returns a case-insensitive mapping of header names to their values.
+ */
+ public Map> headers() {
+ return headers;
+ }
+
+ /**
+ * if present, the response had a body
+ */
+ public Body body() {
+ return body;
+ }
+
+ /**
+ * the request that generated this response
+ */
+ public Request request() {
+ return request;
+ }
+
+ public Charset charset() {
+
+ Collection contentTypeHeaders = headers().get("Content-Type");
+
+ if (contentTypeHeaders != null) {
+ for (String contentTypeHeader : contentTypeHeaders) {
+ String[] contentTypeParmeters = contentTypeHeader.split(";");
+ if (contentTypeParmeters.length > 1) {
+ String[] charsetParts = contentTypeParmeters[1].split("=");
+ if (charsetParts.length == 2 && "charset".equalsIgnoreCase(charsetParts[0].trim())) {
+ return Charset.forName(charsetParts[1]);
+ }
+ }
+ }
+ }
+
+ return Util.UTF_8;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder("HTTP/1.1 ").append(status);
+ if (reason != null)
+ builder.append(' ').append(reason);
+ builder.append('\n');
+ for (String field : headers.keySet()) {
+ for (String value : valuesOrEmpty(headers, field)) {
+ builder.append(field).append(": ").append(value).append('\n');
+ }
+ }
+ if (body != null)
+ builder.append('\n').append(body);
+ return builder.toString();
+ }
+
+ @Override
+ public void close() {
+ Util.ensureClosed(body);
+ }
+
+ public interface Body extends Closeable {
+
+ /**
+ * length in bytes, if known. Null if unknown or greater than {@link Integer#MAX_VALUE}.
+ *
+ *
+ *
+ *
+ * Note
+ * This is an integer as most implementations cannot do bodies greater than 2GB.
+ */
+ Integer length();
+
+ /**
+ * True if {@link #asInputStream()} and {@link #asReader()} can be called more than once.
+ */
+ boolean isRepeatable();
+
+ /**
+ * It is the responsibility of the caller to close the stream.
+ */
+ InputStream asInputStream() throws IOException;
+
+ /**
+ * It is the responsibility of the caller to close the stream.
+ *
+ * @deprecated favor {@link Body#asReader(Charset)}
+ */
+ @Deprecated
+ default Reader asReader() throws IOException {
+ return asReader(StandardCharsets.UTF_8);
+ }
+
+ /**
+ * It is the responsibility of the caller to close the stream.
+ */
+ Reader asReader(Charset charset) throws IOException;
+ }
+
+ private static final class InputStreamBody implements Response.Body {
+
+ private final InputStream inputStream;
+ private final Integer length;
+
+ private InputStreamBody(InputStream inputStream, Integer length) {
+ this.inputStream = inputStream;
+ this.length = length;
+ }
+
+ private static Body orNull(InputStream inputStream, Integer length) {
+ if (inputStream == null) {
+ return null;
+ }
+ return new InputStreamBody(inputStream, length);
+ }
+
+ @Override
+ public Integer length() {
+ return length;
+ }
+
+ @Override
+ public boolean isRepeatable() {
+ return false;
+ }
+
+ @Override
+ public InputStream asInputStream() {
+ return inputStream;
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public Reader asReader() {
+ return new InputStreamReader(inputStream, UTF_8);
+ }
+
+ @Override
+ public Reader asReader(Charset charset) throws IOException {
+ checkNotNull(charset, "charset should not be null");
+ return new InputStreamReader(inputStream, charset);
+ }
+
+ @Override
+ public void close() throws IOException {
+ inputStream.close();
+ }
+
+ }
+
+ private static final class ByteArrayBody implements Response.Body {
+
+ private final byte[] data;
+
+ public ByteArrayBody(byte[] data) {
+ this.data = data;
+ }
+
+ private static Body orNull(byte[] data) {
+ if (data == null) {
+ return null;
+ }
+ return new ByteArrayBody(data);
+ }
+
+ private static Body orNull(String text, Charset charset) {
+ if (text == null) {
+ return null;
+ }
+ checkNotNull(charset, "charset");
+ return new ByteArrayBody(text.getBytes(charset));
+ }
+
+ @Override
+ public Integer length() {
+ return data.length;
+ }
+
+ @Override
+ public boolean isRepeatable() {
+ return true;
+ }
+
+ @Override
+ public InputStream asInputStream() throws IOException {
+ return new ByteArrayInputStream(data);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public Reader asReader() throws IOException {
+ return new InputStreamReader(asInputStream(), UTF_8);
+ }
+
+ @Override
+ public Reader asReader(Charset charset) throws IOException {
+ checkNotNull(charset, "charset should not be null");
+ return new InputStreamReader(asInputStream(), charset);
+ }
+
+ @Override
+ public void close() throws IOException {}
+
+ }
+
+ private static Map> caseInsensitiveCopyOf(Map> headers) {
+ Map> result =
+ new TreeMap>(String.CASE_INSENSITIVE_ORDER);
+
+ for (Map.Entry> entry : headers.entrySet()) {
+ String headerName = entry.getKey();
+ if (!result.containsKey(headerName)) {
+ result.put(headerName.toLowerCase(Locale.ROOT), new LinkedList());
+ }
+ result.get(headerName).addAll(entry.getValue());
+ }
+ return result;
+ }
+}
diff --git a/core/src/main/java/feign/ResponseMapper.java b/core/src/main/java/feign/ResponseMapper.java
new file mode 100644
index 0000000000..403ec6fc83
--- /dev/null
+++ b/core/src/main/java/feign/ResponseMapper.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright 2012-2020 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.Type;
+
+/**
+ * Map function to apply to the response before decoding it.
+ *
+ *
+ *
+ *
+ *
+ *
+ * relationship to JAXRS 2.0
+ *
+ * This call is similar to {@code
+ * javax.ws.rs.client.WebTarget.request()}, except that we expect transient, but necessary
+ * decoration to be applied on invocation.
+ */
+ public Request apply(RequestTemplate input);
+
+ public static class HardCodedTarget implements Target {
+
+ private final Class type;
+ private final String name;
+ private final String url;
+
+ public HardCodedTarget(Class type, String url) {
+ this(type, url, url);
+ }
+
+ public HardCodedTarget(Class type, String name, String url) {
+ this.type = checkNotNull(type, "type");
+ this.name = checkNotNull(emptyToNull(name), "name");
+ this.url = checkNotNull(emptyToNull(url), "url");
+ }
+
+ @Override
+ public Class type() {
+ return type;
+ }
+
+ @Override
+ public String name() {
+ return name;
+ }
+
+ @Override
+ public String url() {
+ return url;
+ }
+
+ /* no authentication or other special activity. just insert the url. */
+ @Override
+ public Request apply(RequestTemplate input) {
+ if (input.url().indexOf("http") != 0) {
+ input.target(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..90d35c8bd0
--- /dev/null
+++ b/core/src/main/java/feign/Types.java
@@ -0,0 +1,468 @@
+/**
+ * Copyright 2012-2020 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.NoSuchElementException;
+
+/**
+ * Static methods for working with types.
+ *
+ * @author Bob Lee
+ * @author Jesse Wilson
+ */
+public final class Types {
+
+ private static final Type[] EMPTY_TYPE_ARRAY = new Type[0];
+
+ private Types() {
+ // No instances.
+ }
+
+ public 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..9946919dd8
--- /dev/null
+++ b/core/src/main/java/feign/Util.java
@@ -0,0 +1,363 @@
+/**
+ * Copyright 2012-2020 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.Buffer;
+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.Optional;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+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;
+ }
+
+ /**
+ * Removes values from the array that meet the criteria for removal via the supplied
+ * {@link Predicate} value
+ */
+ @SuppressWarnings("unchecked")
+ public static T[] removeValues(T[] values, Predicate shouldRemove, Class type) {
+ Collection collection = new ArrayList<>(values.length);
+ for (T value : values) {
+ if (shouldRemove.negate().test(value)) {
+ collection.add(value);
+ }
+ }
+ T[] array = (T[]) Array.newInstance(type, collection.size());
+ return collection.toArray(array);
+ }
+
+ /**
+ * 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) {
+ Collection values = map.get(key);
+ return values != null ? values : 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.getOrDefault(Types.getRawType(type), () -> null).get();
+ }
+
+ private static final Map, Supplier> EMPTIES;
+ static {
+ final Map, Supplier> empties = new LinkedHashMap, Supplier>();
+ 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, Collections::emptyIterator);
+ empties.put(List.class, Collections::emptyList);
+ empties.put(Map.class, Collections::emptyMap);
+ empties.put(Set.class, Collections::emptySet);
+ empties.put(Optional.class, Optional::empty);
+ empties.put(Stream.class, Stream::empty);
+ 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 charBuf = CharBuffer.allocate(BUF_SIZE);
+ // must cast to super class Buffer otherwise break when running with java 11
+ Buffer buf = charBuf;
+ while (reader.read(charBuf) != -1) {
+ buf.flip();
+ to.append(charBuf);
+ 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;
+ }
+ }
+
+ /**
+ * If the provided String is not null or empty.
+ *
+ * @param value to evaluate.
+ * @return true of the value is not null and not empty.
+ */
+ public static boolean isNotBlank(String value) {
+ return value != null && !value.isEmpty();
+ }
+
+ /**
+ * If the provided String is null or empty.
+ *
+ * @param value to evaluate.
+ * @return true if the value is null or empty.
+ */
+ public static boolean isBlank(String value) {
+ return value == null || value.isEmpty();
+ }
+}
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..2cea74f49d
--- /dev/null
+++ b/core/src/main/java/feign/auth/Base64.java
@@ -0,0 +1,158 @@
+/**
+ * Copyright 2012-2020 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..30db075c0a
--- /dev/null
+++ b/core/src/main/java/feign/auth/BasicAuthRequestInterceptor.java
@@ -0,0 +1,68 @@
+/**
+ * Copyright 2012-2020 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..acf2be7271
--- /dev/null
+++ b/core/src/main/java/feign/codec/DecodeException.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright 2012-2020 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 feign.Request;
+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(int status, String message, Request request) {
+ super(status, checkNotNull(message, "message"), request);
+ }
+
+ /**
+ * @param message possibly null reason for the failure.
+ * @param cause the cause of the error.
+ */
+ public DecodeException(int status, String message, Request request, Throwable cause) {
+ super(status, message, request, 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..8896888627
--- /dev/null
+++ b/core/src/main/java/feign/codec/Decoder.java
@@ -0,0 +1,95 @@
+/**
+ * Copyright 2012-2020 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 || response.status() == 204)
+ 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..4ae19bcfef
--- /dev/null
+++ b/core/src/main/java/feign/codec/EncodeException.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright 2012-2020 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 static feign.Util.checkNotNull;
+import feign.FeignException;
+
+/**
+ * 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(-1, 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(-1, 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..c650774fb2
--- /dev/null
+++ b/core/src/main/java/feign/codec/Encoder.java
@@ -0,0 +1,98 @@
+/**
+ * Copyright 2012-2020 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..6fad000c42
--- /dev/null
+++ b/core/src/main/java/feign/codec/ErrorDecoder.java
@@ -0,0 +1,160 @@
+/**
+ * Copyright 2012-2020 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 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.SECONDS;
+import feign.FeignException;
+import feign.Response;
+import feign.RetryableException;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Map;
+
+/**
+ * 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(
+ response.status(),
+ exception.getMessage(),
+ response.request().httpMethod(),
+ exception,
+ retryAfter,
+ response.request());
+ }
+ 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]+\\.?0*$")) {
+ retryAfter = retryAfter.replaceAll("\\.0*$", "");
+ 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..2e6143f9cc
--- /dev/null
+++ b/core/src/main/java/feign/codec/StringDecoder.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright 2012-2020 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(Util.UTF_8));
+ }
+ throw new DecodeException(response.status(),
+ format("%s is not a type supported by this decoder.", type), response.request());
+ }
+}
diff --git a/core/src/main/java/feign/optionals/OptionalDecoder.java b/core/src/main/java/feign/optionals/OptionalDecoder.java
new file mode 100644
index 0000000000..063c584d23
--- /dev/null
+++ b/core/src/main/java/feign/optionals/OptionalDecoder.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright 2012-2020 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.optionals;
+
+import feign.Response;
+import feign.Util;
+import feign.codec.Decoder;
+import java.io.IOException;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.Objects;
+import java.util.Optional;
+
+public final class OptionalDecoder implements Decoder {
+ final Decoder delegate;
+
+ public OptionalDecoder(Decoder delegate) {
+ Objects.requireNonNull(delegate, "Decoder must not be null. ");
+ this.delegate = delegate;
+ }
+
+ @Override
+ public Object decode(Response response, Type type) throws IOException {
+ if (!isOptional(type)) {
+ return delegate.decode(response, type);
+ }
+ if (response.status() == 404 || response.status() == 204) {
+ return Optional.empty();
+ }
+ Type enclosedType = Util.resolveLastTypeParameter(type, Optional.class);
+ return Optional.ofNullable(delegate.decode(response, enclosedType));
+ }
+
+ static boolean isOptional(Type type) {
+ if (!(type instanceof ParameterizedType)) {
+ return false;
+ }
+ ParameterizedType parameterizedType = (ParameterizedType) type;
+ return parameterizedType.getRawType().equals(Optional.class);
+ }
+}
diff --git a/core/src/main/java/feign/querymap/BeanQueryMapEncoder.java b/core/src/main/java/feign/querymap/BeanQueryMapEncoder.java
new file mode 100644
index 0000000000..8c473b7c64
--- /dev/null
+++ b/core/src/main/java/feign/querymap/BeanQueryMapEncoder.java
@@ -0,0 +1,90 @@
+/**
+ * Copyright 2012-2020 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.querymap;
+
+import feign.Param;
+import feign.QueryMapEncoder;
+import feign.codec.EncodeException;
+import java.beans.IntrospectionException;
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.*;
+
+/**
+ * the query map will be generated using java beans accessible getter property as query parameter
+ * names.
+ *
+ * eg: "/uri?name={name}&number={number}"
+ *
+ * order of included query parameters not guaranteed, and as usual, if any value is null, it will be
+ * left out
+ */
+public class BeanQueryMapEncoder 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 propertyNameToValue = new HashMap();
+ for (PropertyDescriptor pd : metadata.objectProperties) {
+ Method method = pd.getReadMethod();
+ Object value = method.invoke(object);
+ if (value != null && value != object) {
+ Param alias = method.getAnnotation(Param.class);
+ String name = alias != null ? alias.value() : pd.getName();
+ propertyNameToValue.put(name, value);
+ }
+ }
+ return propertyNameToValue;
+ } catch (IllegalAccessException | IntrospectionException | InvocationTargetException e) {
+ throw new EncodeException("Failure encoding object into query map", e);
+ }
+ }
+
+ private ObjectParamMetadata getMetadata(Class> objectType) throws IntrospectionException {
+ 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 objectProperties;
+
+ private ObjectParamMetadata(List objectProperties) {
+ this.objectProperties = Collections.unmodifiableList(objectProperties);
+ }
+
+ private static ObjectParamMetadata parseObjectType(Class> type)
+ throws IntrospectionException {
+ List properties = new ArrayList();
+
+ for (PropertyDescriptor pd : Introspector.getBeanInfo(type).getPropertyDescriptors()) {
+ boolean isGetterMethod = pd.getReadMethod() != null && !"class".equals(pd.getName());
+ if (isGetterMethod) {
+ properties.add(pd);
+ }
+ }
+
+ return new ObjectParamMetadata(properties);
+ }
+ }
+}
diff --git a/core/src/main/java/feign/querymap/FieldQueryMapEncoder.java b/core/src/main/java/feign/querymap/FieldQueryMapEncoder.java
new file mode 100644
index 0000000000..7f239a0712
--- /dev/null
+++ b/core/src/main/java/feign/querymap/FieldQueryMapEncoder.java
@@ -0,0 +1,86 @@
+/**
+ * Copyright 2012-2020 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.querymap;
+
+import feign.Param;
+import feign.QueryMapEncoder;
+import feign.codec.EncodeException;
+import java.lang.reflect.Field;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * the query map will be generated using member variable names as query parameter names.
+ *
+ * eg: "/uri?name={name}&number={number}"
+ *
+ * order of included query parameters not guaranteed, and as usual, if any value is null, it will be
+ * left out
+ */
+public class FieldQueryMapEncoder 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) {
+ Param alias = field.getAnnotation(Param.class);
+ String name = alias != null ? alias.value() : field.getName();
+ fieldNameToValue.put(name, 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 allFields = new ArrayList();
+
+ for (Class currentClass = type; currentClass != null; currentClass =
+ currentClass.getSuperclass()) {
+ Collections.addAll(allFields, currentClass.getDeclaredFields());
+ }
+
+ return new ObjectParamMetadata(allFields.stream()
+ .filter(field -> !field.isSynthetic())
+ .peek(field -> field.setAccessible(true))
+ .collect(Collectors.toList()));
+ }
+ }
+}
diff --git a/core/src/main/java/feign/stream/StreamDecoder.java b/core/src/main/java/feign/stream/StreamDecoder.java
new file mode 100644
index 0000000000..5d884f4a81
--- /dev/null
+++ b/core/src/main/java/feign/stream/StreamDecoder.java
@@ -0,0 +1,106 @@
+/**
+ * Copyright 2012-2020 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.stream;
+
+import feign.FeignException;
+import feign.Response;
+import feign.codec.Decoder;
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.Iterator;
+import java.util.Spliterators;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+import static feign.Util.ensureClosed;
+
+/**
+ * Iterator based decoder that support streaming.
+ *