Compare commits

...

32 Commits
master ... 4.x

Author SHA1 Message Date
David M. Carr 97286d34d6 Fix NullPointerException when equals or hashCode are called on proxy instance 11 years ago
Bob T Builder 093e73bf06 [Gradle Release Plugin] - new version commit: '4.4.1-SNAPSHOT'. 11 years ago
Bob T Builder 5ba9cb2bce [Gradle Release Plugin] - pre tag commit: '4.4.0'. 11 years ago
adriancole 151e81dd6d 4.4.0-SNAPSHOT 11 years ago
adriancole a113e19e76 Support urls which have query parameters 11 years ago
adriancole 5099bdcf2e issue #55: support iterable query params 11 years ago
David M. Carr 0dd978275b default client: add support for gzip-encoded request bodies (#52) 11 years ago
David M. Carr f1cea1cca5 default client: use custom HostnameVerifier if overridden 11 years ago
Bob T Builder 2af2658209 [Gradle Release Plugin] - new version commit: '4.3.1-SNAPSHOT'. 11 years ago
Bob T Builder 092e093d6b [Gradle Release Plugin] - pre tag commit: '4.3.0'. 11 years ago
adriancole 073e54ee39 Remove overrides = true on codec modules 11 years ago
adriancole c6b2a5918a closes #35 add RequestInterceptor 11 years ago
adriancole 26d04ecc01 4.3.0-SNAPSHOT 11 years ago
adriancole dd02b9b3fd added dagger IDE setup for annotation parsing via gradle idea and eclipse plugins 11 years ago
adriancole 22d1046b88 flattened project structure so that eclipse gradle plugin will work 11 years ago
Bob T Builder ad9ea726ad [Gradle Release Plugin] - new version commit: '4.2.1-SNAPSHOT'. 11 years ago
Bob T Builder bfbbd8e929 [Gradle Release Plugin] - pre tag commit: '4.2.0'. 11 years ago
adriancole fb8f2ac506 Skip query template parameters when corresponding java arg is null 11 years ago
adriancole 27d936a41d issue #44: ensure jax-rs annotations are processes from POV of server interfaces 11 years ago
adriancole 73efa61617 4.2.0-SNAPSHOT 11 years ago
adriancole facf514f66 updated examples to 4.1 11 years ago
Bob T Builder 445cbd9da1 [Gradle Release Plugin] - new version commit: '4.1.1-SNAPSHOT'. 11 years ago
Bob T Builder 4f2166acfb [Gradle Release Plugin] - pre tag commit: '4.1.0'. 11 years ago
adriancole 950b976241 4.1 11 years ago
adriancole 321210d3ee update to dagger 1.1, as bump test deps 11 years ago
adriancole 15b48c1b71 fix issue #31: support @Path on type 11 years ago
adriancole f21b1a0bf3 4.1.0-SNAPSHOT 11 years ago
adriancole 9f77cb5f4c close issue #37: add wikipedia example 11 years ago
adriancole 08365abcfc renamed github example 11 years ago
adriancole aec2234d60 update github example to use feign 4.0 observable 11 years ago
Bob T Builder cd40bacfeb [Gradle Release Plugin] - new version commit: '4.0.1-SNAPSHOT'. 11 years ago
Bob T Builder e0efd41940 [Gradle Release Plugin] - pre tag commit: '4.0.0'. 11 years ago
  1. 2
      .gitignore
  2. 22
      CHANGES.md
  3. 4
      NOTICE
  4. 23
      README.md
  5. 21
      build.gradle
  6. 0
      core/src/main/java/feign/Body.java
  7. 23
      core/src/main/java/feign/Client.java
  8. 0
      core/src/main/java/feign/Contract.java
  9. 19
      core/src/main/java/feign/Feign.java
  10. 0
      core/src/main/java/feign/FeignException.java
  11. 0
      core/src/main/java/feign/Headers.java
  12. 0
      core/src/main/java/feign/Logger.java
  13. 35
      core/src/main/java/feign/MethodHandler.java
  14. 0
      core/src/main/java/feign/MethodMetadata.java
  15. 0
      core/src/main/java/feign/Observable.java
  16. 0
      core/src/main/java/feign/Observer.java
  17. 43
      core/src/main/java/feign/ReflectiveFeign.java
  18. 0
      core/src/main/java/feign/Request.java
  19. 70
      core/src/main/java/feign/RequestInterceptor.java
  20. 0
      core/src/main/java/feign/RequestLine.java
  21. 62
      core/src/main/java/feign/RequestTemplate.java
  22. 0
      core/src/main/java/feign/Response.java
  23. 0
      core/src/main/java/feign/RetryableException.java
  24. 0
      core/src/main/java/feign/Retryer.java
  25. 0
      core/src/main/java/feign/Subscription.java
  26. 2
      core/src/main/java/feign/Target.java
  27. 0
      core/src/main/java/feign/Types.java
  28. 8
      core/src/main/java/feign/Util.java
  29. 0
      core/src/main/java/feign/codec/DecodeException.java
  30. 0
      core/src/main/java/feign/codec/Decoder.java
  31. 0
      core/src/main/java/feign/codec/Decoders.java
  32. 0
      core/src/main/java/feign/codec/EncodeException.java
  33. 0
      core/src/main/java/feign/codec/Encoder.java
  34. 0
      core/src/main/java/feign/codec/ErrorDecoder.java
  35. 0
      core/src/main/java/feign/codec/IncrementalDecoder.java
  36. 0
      core/src/main/java/feign/codec/SAXDecoder.java
  37. 0
      core/src/main/java/feign/codec/StringDecoder.java
  38. 0
      core/src/main/java/feign/codec/StringIncrementalDecoder.java
  39. 26
      core/src/test/java/feign/AcceptAllHostnameVerifier.java
  40. 0
      core/src/test/java/feign/DefaultContractTest.java
  41. 0
      core/src/test/java/feign/DefaultRetryerTest.java
  42. 344
      core/src/test/java/feign/FeignTest.java
  43. 41
      core/src/test/java/feign/GZIPStreams.java
  44. 167
      core/src/test/java/feign/LoggerTest.java
  45. 70
      core/src/test/java/feign/RequestTemplateTest.java
  46. 193
      core/src/test/java/feign/TrustingSSLSocketFactory.java
  47. 0
      core/src/test/java/feign/UtilTest.java
  48. 0
      core/src/test/java/feign/codec/DefaultErrorDecoderTest.java
  49. 0
      core/src/test/java/feign/codec/RetryAfterDecoderTest.java
  50. 0
      core/src/test/java/feign/examples/AWSSignatureVersion4.java
  51. 2
      core/src/test/java/feign/examples/GitHubExample.java
  52. 2
      core/src/test/java/feign/examples/IAMExample.java
  53. BIN
      core/src/test/resources/keystore.jks
  54. 178
      dagger.gradle
  55. 8
      example-github/build.gradle
  56. 78
      example-github/src/main/java/feign/example/github/GitHubExample.java
  57. 49
      example-wikipedia/build.gradle
  58. 87
      example-wikipedia/src/main/java/feign/example/wikipedia/ResponseDecoder.java
  59. 145
      example-wikipedia/src/main/java/feign/example/wikipedia/WikipediaExample.java
  60. 114
      feign-core/src/test/java/feign/TrustingSSLSocketFactory.java
  61. 76
      feign-jaxrs/src/test/java/feign/jaxrs/examples/IAMExample.java
  62. 2
      gradle.properties
  63. 0
      gson/README.md
  64. 2
      gson/src/main/java/feign/gson/GsonModule.java
  65. 52
      gson/src/test/java/feign/gson/GsonModuleTest.java
  66. 37
      jaxrs/README.md
  67. 53
      jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java
  68. 120
      jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java
  69. 0
      jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java
  70. 0
      ribbon/README.md
  71. 0
      ribbon/src/main/java/feign/ribbon/LBClient.java
  72. 0
      ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java
  73. 0
      ribbon/src/main/java/feign/ribbon/RibbonModule.java
  74. 0
      ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java
  75. 0
      ribbon/src/test/java/feign/ribbon/RibbonClientTest.java
  76. 6
      settings.gradle

2
.gitignore vendored

@ -63,6 +63,8 @@ atlassian-ide-plugin.xml @@ -63,6 +63,8 @@ atlassian-ide-plugin.xml
.project
.settings
.metadata
.factorypath
.generated
# NetBeans specific files/directories
.nbattrs

22
CHANGES.md

@ -1,3 +1,25 @@ @@ -1,3 +1,25 @@
### 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<T>` for an async equiv of `Iterable<T>`.

4
NOTICE

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
Feign
Copyright 2013 Netflix, Inc.
Portions of this software developed by Commerce Technologies, Inc.

23
README.md

@ -37,6 +37,25 @@ public static void main(String... args) { @@ -37,6 +37,25 @@ public static void main(String... args) {
Feign includes a fully functional json codec in the `feign-gson` extension. See the `Decoder` section for how to write your own.
### Request Interceptors
When you need to change all requests, regardless of their target, you'll want to configure a `RequestInterceptor`.
For example, if you are acting as an intermediary, you might want to propagate the `X-Forwarded-For` header.
```
@Module(library = true)
static class ForwardedForInterceptor implements RequestInterceptor {
@Provides(type = SET) RequestInterceptor provideThis() {
return this;
}
@Override public void apply(RequestTemplate template) {
template.header("X-Forwarded-For", "origin.host.com");
}
}
...
GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule(), new ForwardedForInterceptor());
```
### Observable Methods
If specified as the last return type of a method `Observable<T>` will invoke a new http request for each call to `subscribe()`. This is the async equivalent to an `Iterable`.
Here's how one looks:
@ -118,7 +137,7 @@ The `GsonModule` in the `feign-gson` extension configures a (`Decoder.TextStream @@ -118,7 +137,7 @@ The `GsonModule` in the `feign-gson` extension configures a (`Decoder.TextStream
Here's how you could write this yourself, using whatever library you prefer:
```java
@Module(overrides = true, library = true)
@Module(library = true)
static class JsonModule {
@Provides(type = SET) Decoder decoder(final JsonParser parser) {
return new Decoder.TextStream<Object>() {
@ -196,7 +215,7 @@ If you have to only grab a single field from a server response, you may find reg @@ -196,7 +215,7 @@ If you have to only grab a single field from a server response, you may find reg
Here's how our IAM example grabs only one xml element from a response.
```java
@Module(overrides = true, library = true)
@Module(library = true)
static class IAMModule {
@Provides(type = SET) Decoder arnDecoder() {
return Decoders.firstGroup("<Arn>([\\S&&[^<]]+)</Arn>");

21
build.gradle

@ -22,8 +22,10 @@ apply from: file('gradle/maven.gradle') @@ -22,8 +22,10 @@ apply from: file('gradle/maven.gradle')
apply from: file('gradle/check.gradle')
apply from: file('gradle/license.gradle')
apply from: file('gradle/release.gradle')
apply plugin: 'idea'
subprojects {
apply from: rootProject.file('dagger.gradle')
group = "com.netflix.${githubProjectName}" // TEMPLATE: Set to organization of project
}
@ -35,13 +37,11 @@ project(':feign-core') { @@ -35,13 +37,11 @@ project(':feign-core') {
}
dependencies {
compile 'com.squareup.dagger:dagger:1.0.1'
provided 'com.squareup.dagger:dagger-compiler:1.0.1'
testCompile 'com.google.guava:guava:14.0.1'
testCompile 'com.google.code.gson:gson:2.2.4'
testCompile 'com.fasterxml.jackson.core:jackson-databind:2.2.2'
testCompile 'org.testng:testng:6.8.1'
testCompile 'com.google.mockwebserver:mockwebserver:20130505'
testCompile 'org.testng:testng:6.8.5'
testCompile 'com.google.mockwebserver:mockwebserver:20130706'
}
}
@ -55,8 +55,7 @@ project(':feign-gson') { @@ -55,8 +55,7 @@ project(':feign-gson') {
dependencies {
compile project(':feign-core')
compile 'com.google.code.gson:gson:2.2.4'
provided 'com.squareup.dagger:dagger-compiler:1.0.1'
testCompile 'org.testng:testng:6.8.1'
testCompile 'org.testng:testng:6.8.5'
}
}
@ -70,12 +69,9 @@ project(':feign-jaxrs') { @@ -70,12 +69,9 @@ project(':feign-jaxrs') {
dependencies {
compile project(':feign-core')
compile 'javax.ws.rs:jsr311-api:1.1.1'
provided 'com.squareup.dagger:dagger-compiler:1.0.1'
// for example classes
testCompile project(':feign-core').sourceSets.test.output
testCompile project(':feign-gson')
testCompile 'com.google.guava:guava:14.0.1'
testCompile 'org.testng:testng:6.8.1'
testCompile 'org.testng:testng:6.8.5'
}
}
@ -89,8 +85,7 @@ project(':feign-ribbon') { @@ -89,8 +85,7 @@ project(':feign-ribbon') {
dependencies {
compile project(':feign-core')
compile 'com.netflix.ribbon:ribbon-core:0.2.0'
provided 'com.squareup.dagger:dagger-compiler:1.0.1'
testCompile 'org.testng:testng:6.8.1'
testCompile 'com.google.mockwebserver:mockwebserver:20130505'
testCompile 'org.testng:testng:6.8.5'
testCompile 'com.google.mockwebserver:mockwebserver:20130706'
}
}

0
feign-core/src/main/java/feign/Body.java → core/src/main/java/feign/Body.java

23
feign-core/src/main/java/feign/Client.java → core/src/main/java/feign/Client.java

@ -26,15 +26,19 @@ import java.util.Collection; @@ -26,15 +26,19 @@ import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.GZIPOutputStream;
import javax.inject.Inject;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
import dagger.Lazy;
import feign.Request.Options;
import static feign.Util.CONTENT_ENCODING;
import static feign.Util.CONTENT_LENGTH;
import static feign.Util.ENCODING_GZIP;
import static feign.Util.UTF_8;
/**
@ -55,9 +59,11 @@ public interface Client { @@ -55,9 +59,11 @@ public interface Client {
public static class Default implements Client {
private final Lazy<SSLSocketFactory> sslContextFactory;
private final Lazy<HostnameVerifier> hostnameVerifier;
@Inject public Default(Lazy<SSLSocketFactory> sslContextFactory) {
@Inject public Default(Lazy<SSLSocketFactory> sslContextFactory, Lazy<HostnameVerifier> hostnameVerifier) {
this.sslContextFactory = sslContextFactory;
this.hostnameVerifier = hostnameVerifier;
}
@Override public Response execute(Request request, Options options) throws IOException {
@ -70,6 +76,7 @@ public interface Client { @@ -70,6 +76,7 @@ public interface Client {
if (connection instanceof HttpsURLConnection) {
HttpsURLConnection sslCon = (HttpsURLConnection) connection;
sslCon.setSSLSocketFactory(sslContextFactory.get());
sslCon.setHostnameVerifier(hostnameVerifier.get());
}
connection.setConnectTimeout(options.connectTimeoutMillis());
connection.setReadTimeout(options.readTimeoutMillis());
@ -77,13 +84,20 @@ public interface Client { @@ -77,13 +84,20 @@ public interface Client {
connection.setInstanceFollowRedirects(true);
connection.setRequestMethod(request.method());
Collection<String> contentEncodingValues = request.headers().get(CONTENT_ENCODING);
boolean gzipEncodedRequest = contentEncodingValues != null && contentEncodingValues.contains(ENCODING_GZIP);
Integer contentLength = null;
for (String field : request.headers().keySet()) {
for (String value : request.headers().get(field)) {
if (field.equals(CONTENT_LENGTH)) {
contentLength = Integer.valueOf(value);
if (!gzipEncodedRequest) {
contentLength = Integer.valueOf(value);
connection.addRequestProperty(field, value);
}
} else {
connection.addRequestProperty(field, value);
}
connection.addRequestProperty(field, value);
}
}
@ -95,6 +109,9 @@ public interface Client { @@ -95,6 +109,9 @@ public interface Client {
}
connection.setDoOutput(true);
OutputStream out = connection.getOutputStream();
if (gzipEncodedRequest) {
out = new GZIPOutputStream(out);
}
try {
out.write(request.body().getBytes(UTF_8));
} finally {

0
feign-core/src/main/java/feign/Contract.java → core/src/main/java/feign/Contract.java

19
feign-core/src/main/java/feign/Feign.java → core/src/main/java/feign/Feign.java

@ -29,6 +29,8 @@ import feign.codec.IncrementalDecoder; @@ -29,6 +29,8 @@ import feign.codec.IncrementalDecoder;
import javax.inject.Named;
import javax.inject.Singleton;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
import java.io.Closeable;
import java.lang.reflect.Method;
@ -104,6 +106,11 @@ public abstract class Feign implements Closeable { @@ -104,6 +106,11 @@ public abstract class Feign implements Closeable {
return SSLSocketFactory.class.cast(SSLSocketFactory.getDefault());
}
@Provides
HostnameVerifier hostnameVerifier() {
return HttpsURLConnection.getDefaultHostnameVerifier();
}
@Provides Client httpClient(Client.Default client) {
return client;
}
@ -124,18 +131,6 @@ public abstract class Feign implements Closeable { @@ -124,18 +131,6 @@ public abstract class Feign implements Closeable {
return new Options();
}
@Provides Set<Encoder> noEncoders() {
return Collections.emptySet();
}
@Provides Set<Decoder> noDecoders() {
return Collections.emptySet();
}
@Provides Set<IncrementalDecoder> noIncrementalDecoders() {
return Collections.emptySet();
}
/**
* Used for both http invocation and decoding when observers are used.
*/

0
feign-core/src/main/java/feign/FeignException.java → core/src/main/java/feign/FeignException.java

0
feign-core/src/main/java/feign/Headers.java → core/src/main/java/feign/Headers.java

0
feign-core/src/main/java/feign/Logger.java → core/src/main/java/feign/Logger.java

35
feign-core/src/main/java/feign/MethodHandler.java → core/src/main/java/feign/MethodHandler.java

@ -27,6 +27,7 @@ import javax.inject.Named; @@ -27,6 +27,7 @@ import javax.inject.Named;
import javax.inject.Provider;
import java.io.IOException;
import java.io.Reader;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@ -44,29 +45,31 @@ interface MethodHandler { @@ -44,29 +45,31 @@ interface MethodHandler {
private final Client client;
private final Lazy<Executor> httpExecutor;
private final Provider<Retryer> retryer;
private final Set<RequestInterceptor> requestInterceptors;
private final Logger logger;
private final Provider<Logger.Level> logLevel;
@Inject Factory(Client client, @Named("http") Lazy<Executor> httpExecutor, Provider<Retryer> retryer, Logger logger,
Provider<Logger.Level> logLevel) {
@Inject Factory(Client client, @Named("http") Lazy<Executor> httpExecutor, Provider<Retryer> retryer,
Set<RequestInterceptor> requestInterceptors, Logger logger, Provider<Logger.Level> logLevel) {
this.client = checkNotNull(client, "client");
this.httpExecutor = checkNotNull(httpExecutor, "httpExecutor");
this.retryer = checkNotNull(retryer, "retryer");
this.requestInterceptors = checkNotNull(requestInterceptors, "requestInterceptors");
this.logger = checkNotNull(logger, "logger");
this.logLevel = checkNotNull(logLevel, "logLevel");
}
public MethodHandler create(Target<?> target, MethodMetadata md, BuildTemplateFromArgs buildTemplateFromArgs,
Options options, Decoder.TextStream<?> decoder, ErrorDecoder errorDecoder) {
return new SynchronousMethodHandler(target, client, retryer, logger, logLevel, md, buildTemplateFromArgs, options,
decoder, errorDecoder);
return new SynchronousMethodHandler(target, client, retryer, requestInterceptors, logger, logLevel, md,
buildTemplateFromArgs, options, decoder, errorDecoder);
}
public MethodHandler create(Target<?> target, MethodMetadata md, BuildTemplateFromArgs buildTemplateFromArgs,
Options options, IncrementalDecoder.TextStream<?> incrementalDecoder,
ErrorDecoder errorDecoder) {
ObserverHandler observerHandler = new ObserverHandler(target, client, retryer, logger, logLevel, md,
buildTemplateFromArgs, options, incrementalDecoder, errorDecoder, httpExecutor);
ObserverHandler observerHandler = new ObserverHandler(target, client, retryer, requestInterceptors, logger,
logLevel, md, buildTemplateFromArgs, options, incrementalDecoder, errorDecoder, httpExecutor);
return new ObservableMethodHandler(observerHandler);
}
}
@ -106,12 +109,14 @@ interface MethodHandler { @@ -106,12 +109,14 @@ interface MethodHandler {
private final Lazy<Executor> httpExecutor;
private final IncrementalDecoder.TextStream<?> incrementalDecoder;
private ObserverHandler(Target<?> target, Client client, Provider<Retryer> retryer, Logger logger,
private ObserverHandler(Target<?> target, Client client, Provider<Retryer> retryer,
Set<RequestInterceptor> requestInterceptors, Logger logger,
Provider<Logger.Level> logLevel, MethodMetadata metadata,
BuildTemplateFromArgs buildTemplateFromArgs, Options options,
IncrementalDecoder.TextStream<?> incrementalDecoder, ErrorDecoder errorDecoder,
Lazy<Executor> httpExecutor) {
super(target, client, retryer, logger, logLevel, metadata, buildTemplateFromArgs, options, errorDecoder);
super(target, client, retryer, requestInterceptors, logger, logLevel, metadata, buildTemplateFromArgs, options,
errorDecoder);
this.httpExecutor = checkNotNull(httpExecutor, "httpExecutor for %s", target);
this.incrementalDecoder = checkNotNull(incrementalDecoder, "incrementalDecoder for %s", target);
}
@ -185,11 +190,13 @@ interface MethodHandler { @@ -185,11 +190,13 @@ interface MethodHandler {
static class SynchronousMethodHandler extends BaseMethodHandler {
private final Decoder.TextStream<?> decoder;
private SynchronousMethodHandler(Target<?> target, Client client, Provider<Retryer> retryer, Logger logger,
private SynchronousMethodHandler(Target<?> target, Client client, Provider<Retryer> retryer,
Set<RequestInterceptor> requestInterceptors, Logger logger,
Provider<Logger.Level> logLevel, MethodMetadata metadata,
BuildTemplateFromArgs buildTemplateFromArgs, Options options,
Decoder.TextStream<?> decoder, ErrorDecoder errorDecoder) {
super(target, client, retryer, logger, logLevel, metadata, buildTemplateFromArgs, options, errorDecoder);
super(target, client, retryer, requestInterceptors, logger, logLevel, metadata, buildTemplateFromArgs, options,
errorDecoder);
this.decoder = checkNotNull(decoder, "decoder for %s", target);
}
@ -215,18 +222,21 @@ interface MethodHandler { @@ -215,18 +222,21 @@ interface MethodHandler {
protected final Target<?> target;
protected final Client client;
protected final Provider<Retryer> retryer;
protected final Set<RequestInterceptor> requestInterceptors;
protected final Logger logger;
protected final Provider<Logger.Level> logLevel;
protected final BuildTemplateFromArgs buildTemplateFromArgs;
protected final Options options;
protected final ErrorDecoder errorDecoder;
private BaseMethodHandler(Target<?> target, Client client, Provider<Retryer> retryer, Logger logger,
private BaseMethodHandler(Target<?> target, Client client, Provider<Retryer> retryer,
Set<RequestInterceptor> requestInterceptors, Logger logger,
Provider<Logger.Level> logLevel, MethodMetadata metadata,
BuildTemplateFromArgs buildTemplateFromArgs, Options options, ErrorDecoder errorDecoder) {
this.target = checkNotNull(target, "target");
this.client = checkNotNull(client, "client for %s", target);
this.retryer = checkNotNull(retryer, "retryer for %s", target);
this.requestInterceptors = checkNotNull(requestInterceptors, "requestInterceptors for %s", target);
this.logger = checkNotNull(logger, "logger for %s", target);
this.logLevel = checkNotNull(logLevel, "logLevel for %s", target);
this.metadata = checkNotNull(metadata, "metadata for %s", target);
@ -294,6 +304,9 @@ interface MethodHandler { @@ -294,6 +304,9 @@ interface MethodHandler {
}
protected Request targetRequest(RequestTemplate template) {
for (RequestInterceptor interceptor : requestInterceptors) {
interceptor.apply(template);
}
return target.apply(new RequestTemplate(template));
}

0
feign-core/src/main/java/feign/MethodMetadata.java → core/src/main/java/feign/MethodMetadata.java

0
feign-core/src/main/java/feign/Observable.java → core/src/main/java/feign/Observable.java

0
feign-core/src/main/java/feign/Observer.java → core/src/main/java/feign/Observer.java

43
feign-core/src/main/java/feign/ReflectiveFeign.java → core/src/main/java/feign/ReflectiveFeign.java

@ -33,6 +33,7 @@ import java.lang.reflect.Method; @@ -33,6 +33,7 @@ import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
@ -84,6 +85,17 @@ public class ReflectiveFeign extends Feign { @@ -84,6 +85,17 @@ public class ReflectiveFeign extends Feign {
}
@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;
}
}
if ("hashCode".equals(method.getName())) {
return hashCode();
}
return methodToHandler.get(method).invoke(args);
}
@ -92,10 +104,15 @@ public class ReflectiveFeign extends Feign { @@ -92,10 +104,15 @@ public class ReflectiveFeign extends Feign {
}
@Override public boolean equals(Object obj) {
if (this == obj)
if (obj == null) {
return false;
}
if (this == obj) {
return true;
if (FeignInvocationHandler.class != obj.getClass())
}
if (FeignInvocationHandler.class != obj.getClass()) {
return false;
}
FeignInvocationHandler that = FeignInvocationHandler.class.cast(obj);
return this.target.equals(that.target);
}
@ -105,19 +122,29 @@ public class ReflectiveFeign extends Feign { @@ -105,19 +122,29 @@ public class ReflectiveFeign extends Feign {
}
}
@dagger.Module(complete = false, injects = Feign.class, library = true)
@dagger.Module(complete = false, injects = {Feign.class, MethodHandler.Factory.class}, library = true)
public static class Module {
@Provides(type = Provides.Type.SET_VALUES) Set<RequestInterceptor> noRequestInterceptors() {
return Collections.emptySet();
}
@Provides(type = Provides.Type.SET_VALUES) Set<Encoder> noEncoders() {
return Collections.emptySet();
}
@Provides(type = Provides.Type.SET_VALUES) Set<Decoder> noDecoders() {
return Collections.emptySet();
}
@Provides(type = Provides.Type.SET_VALUES) Set<IncrementalDecoder> noIncrementalDecoders() {
return Collections.emptySet();
}
@Provides Feign provideFeign(ReflectiveFeign in) {
return in;
}
}
private static IllegalStateException noConfig(String configKey, Class<?> type) {
return new IllegalStateException(format("no configuration for %s present for %s!", configKey,
type.getSimpleName()));
}
static final class ParseHandlersByName {
private final Contract contract;
private final Options options;

0
feign-core/src/main/java/feign/Request.java → core/src/main/java/feign/Request.java

70
core/src/main/java/feign/RequestInterceptor.java

@ -0,0 +1,70 @@ @@ -0,0 +1,70 @@
/*
* Copyright 2013 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package feign;
/**
* Zero or more {@code RequestInterceptors} may be configured for purposes
* such as adding headers to all requests. No guarantees are give with regards
* to the order that interceptors are applied. Once interceptors are applied,
* {@link Target#apply(RequestTemplate)} is called to create the immutable http
* request sent via {@link Client#execute(Request, feign.Request.Options)}.
* <br>
* <br>
* For example:
* <br>
* <pre>
* public void apply(RequestTemplate input) {
* input.replaceHeader(&quot;X-Auth&quot;, currentToken);
* }
* </pre>
* <br>
* <br><b>Configuration</b><br>
* <br>
* {@code RequestInterceptors} are configured via Dagger
* {@link dagger.Provides.Type#SET set} or
* {@link dagger.Provides.Type#SET_VALUES set values}
* {@link dagger.Provides provider} methods.
* <br>
* <br>
* For example:
* <br>
* <pre>
* {@literal @}Provides(Type = SET) RequestInterceptor addTimestamp(TimestampInterceptor in) {
* return in;
* }
* </pre>
* <br>
* <br><b>Implementation notes</b><br>
* <br>
* Do not add parameters, such as {@code /path/{foo}/bar }
* in your implementation of {@link #apply(RequestTemplate)}.
* <br>
* 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.
* <br>
* <br><br><b>Relationship to Retrofit 1.x</b><br>
* <br>
* 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);
}

0
feign-core/src/main/java/feign/RequestLine.java → core/src/main/java/feign/RequestLine.java

62
feign-core/src/main/java/feign/RequestTemplate.java → core/src/main/java/feign/RequestTemplate.java

@ -23,6 +23,7 @@ import java.util.ArrayList; @@ -23,6 +23,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@ -72,19 +73,8 @@ public final class RequestTemplate implements Serializable { @@ -72,19 +73,8 @@ public final class RequestTemplate implements Serializable {
}
/**
* Targets a template to this target, adding the {@link #url() base url} and
* any authentication headers.
* <br>
* <br>
* For example:
* <br>
* <pre>
* public Request apply(RequestTemplate input) {
* input.insert(0, url());
* input.replaceHeader(&quot;X-Auth&quot;, currentToken);
* return input.asRequest();
* }
* </pre>
* Resolves any templated variables in the requests path, query, or headers
* against the supplied unencoded arguments.
* <br>
* <br><br><b>relationship to JAXRS 2.0</b><br>
* <br>
@ -94,13 +84,11 @@ public final class RequestTemplate implements Serializable { @@ -94,13 +84,11 @@ public final class RequestTemplate implements Serializable {
* just the URL
*/
public RequestTemplate resolve(Map<String, ?> unencoded) {
replaceQueryValues(unencoded);
Map<String, String> encoded = new LinkedHashMap<String, String>();
for (Entry<String, ?> entry : unencoded.entrySet()) {
encoded.put(entry.getKey(), urlEncode(String.valueOf(entry.getValue())));
}
String queryLine = expand(queryLine(), encoded);
queries.clear();
pullAnyQueriesOutOfUrl(new StringBuilder(queryLine));
String resolvedUrl = expand(url.toString(), encoded).replace("%2F", "/");
url = new StringBuilder(resolvedUrl);
@ -215,8 +203,7 @@ public final class RequestTemplate implements Serializable { @@ -215,8 +203,7 @@ public final class RequestTemplate implements Serializable {
/* @see #url() */
public RequestTemplate insert(int pos, CharSequence value) {
url.insert(pos, value);
url = pullAnyQueriesOutOfUrl(url);
url.insert(pos, pullAnyQueriesOutOfUrl(new StringBuilder(value)));
return this;
}
@ -505,6 +492,44 @@ public final class RequestTemplate implements Serializable { @@ -505,6 +492,44 @@ public final class RequestTemplate implements Serializable {
return request().toString();
}
/**
* Replaces query values which are templated with corresponding values from the {@code unencoded} map.
* Any unresolved queries are removed.
*/
public void replaceQueryValues(Map<String, ?> unencoded) {
Iterator<Entry<String, Collection<String>>> iterator = queries.entrySet().iterator();
while (iterator.hasNext()) {
Entry<String, Collection<String>> entry = iterator.next();
if (entry.getValue() == null) {
continue;
}
Collection<String> values = new ArrayList<String>();
for (String value : entry.getValue()) {
if (value.indexOf('{') == 0 && value.indexOf('}') == value.length() - 1) {
Object variableValue = unencoded.get(value.substring(1, value.length() - 1));
// only add non-null expressions
if (variableValue == null) {
continue;
}
if (variableValue instanceof Iterable) {
for (Object val : Iterable.class.cast(variableValue)) {
values.add(urlEncode(String.valueOf(val)));
}
} else {
values.add(urlEncode(String.valueOf(variableValue)));
}
} else {
values.add(value);
}
}
if (values.isEmpty()) {
iterator.remove();
} else {
entry.setValue(values);
}
}
}
public String queryLine() {
if (queries.isEmpty())
return "";
@ -524,6 +549,5 @@ public final class RequestTemplate implements Serializable { @@ -524,6 +549,5 @@ public final class RequestTemplate implements Serializable {
return queryBuilder.insert(0, '?').toString();
}
private static final long serialVersionUID = 1L;
}

0
feign-core/src/main/java/feign/Response.java → core/src/main/java/feign/Response.java

0
feign-core/src/main/java/feign/RetryableException.java → core/src/main/java/feign/RetryableException.java

0
feign-core/src/main/java/feign/Retryer.java → core/src/main/java/feign/Retryer.java

0
feign-core/src/main/java/feign/Subscription.java → core/src/main/java/feign/Subscription.java

2
feign-core/src/main/java/feign/Target.java → core/src/main/java/feign/Target.java

@ -40,7 +40,7 @@ public interface Target<T> { @@ -40,7 +40,7 @@ public interface Target<T> {
/**
* Targets a template to this target, adding the {@link #url() base url} and
* any authentication headers.
* any target-specific headers or query parameters.
* <br>
* <br>
* For example:

0
feign-core/src/main/java/feign/Types.java → core/src/main/java/feign/Types.java

8
feign-core/src/main/java/feign/Util.java → core/src/main/java/feign/Util.java

@ -39,10 +39,18 @@ public class Util { @@ -39,10 +39,18 @@ 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";
// com.google.common.base.Charsets
/**

0
feign-core/src/main/java/feign/codec/DecodeException.java → core/src/main/java/feign/codec/DecodeException.java

0
feign-core/src/main/java/feign/codec/Decoder.java → core/src/main/java/feign/codec/Decoder.java

0
feign-core/src/main/java/feign/codec/Decoders.java → core/src/main/java/feign/codec/Decoders.java

0
feign-core/src/main/java/feign/codec/EncodeException.java → core/src/main/java/feign/codec/EncodeException.java

0
feign-core/src/main/java/feign/codec/Encoder.java → core/src/main/java/feign/codec/Encoder.java

0
feign-core/src/main/java/feign/codec/ErrorDecoder.java → core/src/main/java/feign/codec/ErrorDecoder.java

0
feign-core/src/main/java/feign/codec/IncrementalDecoder.java → core/src/main/java/feign/codec/IncrementalDecoder.java

0
feign-core/src/main/java/feign/codec/SAXDecoder.java → core/src/main/java/feign/codec/SAXDecoder.java

0
feign-core/src/main/java/feign/codec/StringDecoder.java → core/src/main/java/feign/codec/StringDecoder.java

0
feign-core/src/main/java/feign/codec/StringIncrementalDecoder.java → core/src/main/java/feign/codec/StringIncrementalDecoder.java

26
core/src/test/java/feign/AcceptAllHostnameVerifier.java

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
/*
* Copyright 2013 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package feign;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSession;
final class AcceptAllHostnameVerifier implements HostnameVerifier {
@Override
public boolean verify(String s, SSLSession sslSession) {
return true;
}
}

0
feign-core/src/test/java/feign/DefaultContractTest.java → core/src/test/java/feign/DefaultContractTest.java

0
feign-core/src/test/java/feign/DefaultRetryerTest.java → core/src/test/java/feign/DefaultRetryerTest.java

344
feign-core/src/test/java/feign/FeignTest.java → core/src/test/java/feign/FeignTest.java

@ -16,8 +16,11 @@ @@ -16,8 +16,11 @@
package feign;
import com.google.common.base.Joiner;
import com.google.common.io.ByteStreams;
import com.google.common.io.CharStreams;
import com.google.mockwebserver.MockResponse;
import com.google.mockwebserver.MockWebServer;
import com.google.mockwebserver.RecordedRequest;
import com.google.mockwebserver.SocketPolicy;
import dagger.Lazy;
import dagger.Module;
@ -30,6 +33,7 @@ import org.testng.annotations.Test; @@ -30,6 +33,7 @@ import org.testng.annotations.Test;
import javax.inject.Named;
import javax.inject.Singleton;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSocketFactory;
import java.io.IOException;
import java.io.Reader;
@ -45,7 +49,10 @@ import java.util.concurrent.Executors; @@ -45,7 +49,10 @@ import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import static dagger.Provides.Type.SET;
import static feign.Util.UTF_8;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;
@ -78,18 +85,22 @@ public class FeignTest { @@ -78,18 +85,22 @@ public class FeignTest {
@RequestLine("POST /") void body(List<String> contents);
@RequestLine("POST /") @Headers("Content-Encoding: gzip") void gzipBody(List<String> contents);
@RequestLine("POST /") void form(
@Named("customer_name") String customer, @Named("user_name") String user, @Named("password") String password);
@RequestLine("GET /{1}/{2}") Response uriParam(@Named("1") String one, URI endpoint, @Named("2") String two);
@RequestLine("GET /?1={1}&2={2}") Response queryParams(@Named("1") String one, @Named("2") Iterable<String> twos);
@RequestLine("POST /") Observable<Void> observableVoid();
@RequestLine("POST /") Observable<String> observableString();
@RequestLine("POST /") Observable<Response> observableResponse();
@dagger.Module(overrides = true, library = true)
@dagger.Module(library = true)
static class Module {
@Provides(type = SET) Encoder defaultEncoder() {
return new Encoder.Text<Object>() {
@ -106,26 +117,38 @@ public class FeignTest { @@ -106,26 +117,38 @@ public class FeignTest {
}
};
}
}
}
// just run synchronously
@Provides @Singleton @Named("http") Executor httpExecutor() {
return new Executor() {
@Override public void execute(Runnable command) {
command.run();
}
};
}
@Test
public void iterableQueryParams() throws IOException, InterruptedException {
final MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setBody("foo"));
server.play();
try {
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module());
api.queryParams("user", Arrays.asList("apple", "pear"));
assertEquals(server.takeRequest().getRequestLine(), "GET /?1=user&2=apple&2=pear HTTP/1.1");
} finally {
server.shutdown();
}
}
interface OtherTestInterface {
@RequestLine("POST /") String post();
}
@Test
public void observableVoid() throws IOException, InterruptedException {
final MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setResponseCode(200).setBody("foo"));
server.enqueue(new MockResponse().setBody("foo"));
server.play();
try {
TestInterface api = Feign.create(TestInterface.class, server.getUrl("").toString(), new TestInterface.Module());
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(),
new TestInterface.Module(), new RunSynchronous());
final AtomicBoolean success = new AtomicBoolean();
@ -155,11 +178,12 @@ public class FeignTest { @@ -155,11 +178,12 @@ public class FeignTest {
@Test
public void observableResponse() throws IOException, InterruptedException {
final MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setResponseCode(200).setBody("foo"));
server.enqueue(new MockResponse().setBody("foo"));
server.play();
try {
TestInterface api = Feign.create(TestInterface.class, server.getUrl("").toString(), new TestInterface.Module());
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(),
new TestInterface.Module(), new RunSynchronous());
final AtomicBoolean success = new AtomicBoolean();
@ -186,14 +210,26 @@ public class FeignTest { @@ -186,14 +210,26 @@ public class FeignTest {
}
}
@Module(library = true, overrides = true)
static class RunSynchronous {
@Provides @Singleton @Named("http") Executor httpExecutor() {
return new Executor() {
@Override public void execute(Runnable command) {
command.run();
}
};
}
}
@Test
public void incrementString() throws IOException, InterruptedException {
final MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setResponseCode(200).setBody("foo"));
server.enqueue(new MockResponse().setBody("foo"));
server.play();
try {
TestInterface api = Feign.create(TestInterface.class, server.getUrl("").toString(), new TestInterface.Module());
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(),
new TestInterface.Module(), new RunSynchronous());
final AtomicBoolean success = new AtomicBoolean();
@ -223,12 +259,12 @@ public class FeignTest { @@ -223,12 +259,12 @@ public class FeignTest {
@Test
public void multipleObservers() throws IOException, InterruptedException {
final MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setResponseCode(200).setBody("foo"));
server.enqueue(new MockResponse().setResponseCode(200).setBody("foo"));
server.enqueue(new MockResponse().setBody("foo"));
server.enqueue(new MockResponse().setBody("foo"));
server.play();
try {
TestInterface api = Feign.create(TestInterface.class, server.getUrl("").toString(), new TestInterface.Module());
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module());
final CountDownLatch latch = new CountDownLatch(2);
@ -261,11 +297,11 @@ public class FeignTest { @@ -261,11 +297,11 @@ public class FeignTest {
@Test
public void postTemplateParamsResolve() throws IOException, InterruptedException {
final MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setResponseCode(200).setBody("foo"));
server.enqueue(new MockResponse().setBody("foo"));
server.play();
try {
TestInterface api = Feign.create(TestInterface.class, server.getUrl("").toString(), new TestInterface.Module());
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module());
api.login("netflix", "denominator", "password");
assertEquals(new String(server.takeRequest().getBody()),
@ -278,11 +314,11 @@ public class FeignTest { @@ -278,11 +314,11 @@ public class FeignTest {
@Test
public void postFormParams() throws IOException, InterruptedException {
final MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setResponseCode(200).setBody("foo"));
server.enqueue(new MockResponse().setBody("foo"));
server.play();
try {
TestInterface api = Feign.create(TestInterface.class, server.getUrl("").toString(), new TestInterface.Module());
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module());
api.form("netflix", "denominator", "password");
assertEquals(new String(server.takeRequest().getBody()),
@ -295,14 +331,95 @@ public class FeignTest { @@ -295,14 +331,95 @@ public class FeignTest {
@Test
public void postBodyParam() throws IOException, InterruptedException {
final MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setResponseCode(200).setBody("foo"));
server.enqueue(new MockResponse().setBody("foo"));
server.play();
try {
TestInterface api = Feign.create(TestInterface.class, server.getUrl("").toString(), new TestInterface.Module());
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module());
api.body(Arrays.asList("netflix", "denominator", "password"));
assertEquals(new String(server.takeRequest().getBody()), "[netflix, denominator, password]");
RecordedRequest request = server.takeRequest();
assertEquals(request.getHeader("Content-Length"), "32");
assertEquals(new String(request.getBody()), "[netflix, denominator, password]");
} finally {
server.shutdown();
}
}
@Test
public void postGZIPEncodedBodyParam() throws IOException, InterruptedException {
final MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setBody("foo"));
server.play();
try {
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module());
api.gzipBody(Arrays.asList("netflix", "denominator", "password"));
RecordedRequest request = server.takeRequest();
assertNull(request.getHeader("Content-Length"));
byte[] compressedBody = request.getBody();
String uncompressedBody = CharStreams.toString(CharStreams.newReaderSupplier(
GZIPStreams.newInputStreamSupplier(ByteStreams.newInputStreamSupplier(compressedBody)), UTF_8));
assertEquals(uncompressedBody, "[netflix, denominator, password]");
} finally {
server.shutdown();
}
}
@Module(library = true)
static class ForwardedForInterceptor implements RequestInterceptor {
@Provides(type = SET) RequestInterceptor provideThis() {
return this;
}
@Override public void apply(RequestTemplate template) {
template.header("X-Forwarded-For", "origin.host.com");
}
}
@Test
public void singleInterceptor() throws IOException, InterruptedException {
final MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setBody("foo"));
server.play();
try {
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(),
new TestInterface.Module(), new ForwardedForInterceptor());
api.post();
assertEquals(server.takeRequest().getHeader("X-Forwarded-For"), "origin.host.com");
} finally {
server.shutdown();
}
}
@Module(library = true)
static class UserAgentInterceptor implements RequestInterceptor {
@Provides(type = SET) RequestInterceptor provideThis() {
return this;
}
@Override public void apply(RequestTemplate template) {
template.header("User-Agent", "Feign");
}
}
@Test
public void multipleInterceptor() throws IOException, InterruptedException {
final MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setBody("foo"));
server.play();
try {
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(),
new TestInterface.Module(), new ForwardedForInterceptor(), new UserAgentInterceptor());
api.post();
RecordedRequest request = server.takeRequest();
assertEquals(request.getHeader("X-Forwarded-For"), "origin.host.com");
assertEquals(request.getHeader("User-Agent"), "Feign");
} finally {
server.shutdown();
}
@ -314,29 +431,32 @@ public class FeignTest { @@ -314,29 +431,32 @@ public class FeignTest {
String.class)), "TestInterface#uriParam(String,URI,String)");
}
@Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "zone not found")
public void canOverrideErrorDecoder() throws IOException, InterruptedException {
@dagger.Module(overrides = true, includes = TestInterface.Module.class) class Overrides {
@Provides @Singleton ErrorDecoder errorDecoder() {
return new ErrorDecoder.Default() {
@Override
public Exception decode(String methodKey, Response response) {
if (response.status() == 404)
return new IllegalArgumentException("zone not found");
return super.decode(methodKey, response);
}
@dagger.Module(overrides = true, library = true, includes = TestInterface.Module.class)
static class IllegalArgumentExceptionOn404 {
@Provides @Singleton ErrorDecoder errorDecoder() {
return new ErrorDecoder.Default() {
};
}
@Override
public Exception decode(String methodKey, Response response) {
if (response.status() == 404)
return new IllegalArgumentException("zone not found");
return super.decode(methodKey, response);
}
};
}
}
@Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "zone not found")
public void canOverrideErrorDecoder() throws IOException, InterruptedException {
final MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setResponseCode(404).setBody("foo"));
server.play();
try {
TestInterface api = Feign.create(TestInterface.class, server.getUrl("").toString(), new Overrides());
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(),
new IllegalArgumentExceptionOn404());
api.post();
} finally {
@ -347,11 +467,11 @@ public class FeignTest { @@ -347,11 +467,11 @@ public class FeignTest {
@Test public void retriesLostConnectionBeforeRead() throws IOException, InterruptedException {
MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START));
server.enqueue(new MockResponse().setResponseCode(200).setBody("success!".getBytes()));
server.enqueue(new MockResponse().setBody("success!".getBytes()));
server.play();
try {
TestInterface api = Feign.create(TestInterface.class, server.getUrl("").toString(),
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(),
new TestInterface.Module());
api.post();
@ -362,23 +482,26 @@ public class FeignTest { @@ -362,23 +482,26 @@ public class FeignTest {
}
}
@dagger.Module(overrides = true, library = true, includes = TestInterface.Module.class)
static class DecodeFail {
@Provides(type = SET) Decoder decoder() {
return new Decoder.TextStream<String>() {
@Override
public String decode(Reader reader, Type type) throws IOException {
return "fail";
}
};
}
}
public void overrideTypeSpecificDecoder() throws IOException, InterruptedException {
MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setResponseCode(200).setBody("success!".getBytes()));
server.enqueue(new MockResponse().setBody("success!".getBytes()));
server.play();
try {
@dagger.Module(overrides = true, includes = TestInterface.Module.class) class Overrides {
@Provides(type = SET) Decoder decoder() {
return new Decoder.TextStream<String>() {
@Override
public String decode(Reader reader, Type type) throws IOException {
return "fail";
}
};
}
}
TestInterface api = Feign.create(TestInterface.class, server.getUrl("").toString(), new Overrides());
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(),
new DecodeFail());
assertEquals(api.post(), "fail");
} finally {
@ -387,30 +510,33 @@ public class FeignTest { @@ -387,30 +510,33 @@ public class FeignTest {
}
}
@dagger.Module(overrides = true, library = true, includes = TestInterface.Module.class)
static class RetryableExceptionOnRetry {
@Provides(type = SET) Decoder decoder() {
return new StringDecoder() {
@Override
public String decode(Reader reader, Type type) throws RetryableException, IOException {
String string = super.decode(reader, type);
if ("retry!".equals(string))
throw new RetryableException(string, null);
return string;
}
};
}
}
/**
* when you must parse a 2xx status to determine if the operation succeeded or not.
*/
public void retryableExceptionInDecoder() throws IOException, InterruptedException {
MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setResponseCode(200).setBody("retry!".getBytes()));
server.enqueue(new MockResponse().setResponseCode(200).setBody("success!".getBytes()));
server.enqueue(new MockResponse().setBody("retry!".getBytes()));
server.enqueue(new MockResponse().setBody("success!".getBytes()));
server.play();
try {
@dagger.Module(overrides = true, includes = TestInterface.Module.class) class Overrides {
@Provides(type = SET) Decoder decoder() {
return new StringDecoder() {
@Override
public String decode(Reader reader, Type type) throws RetryableException, IOException {
String string = super.decode(reader, type);
if ("retry!".equals(string))
throw new RetryableException(string, null);
return string;
}
};
}
}
TestInterface api = Feign.create(TestInterface.class, server.getUrl("").toString(), new Overrides());
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(),
new RetryableExceptionOnRetry());
assertEquals(api.post(), "success!");
} finally {
@ -419,24 +545,27 @@ public class FeignTest { @@ -419,24 +545,27 @@ public class FeignTest {
}
}
@dagger.Module(overrides = true, library = true, includes = TestInterface.Module.class)
static class IOEOnDecode {
@Provides(type = SET) Decoder decoder() {
return new Decoder.TextStream<String>() {
@Override
public String decode(Reader reader, Type type) throws IOException {
throw new IOException("error reading response");
}
};
}
}
@Test(expectedExceptions = FeignException.class, expectedExceptionsMessageRegExp = "error reading response POST http://.*")
public void doesntRetryAfterResponseIsSent() throws IOException, InterruptedException {
MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setResponseCode(200).setBody("success!".getBytes()));
server.enqueue(new MockResponse().setBody("success!".getBytes()));
server.play();
try {
@dagger.Module(overrides = true, includes = TestInterface.Module.class) class Overrides {
@Provides(type = SET) Decoder decoder() {
return new Decoder.TextStream<String>() {
@Override
public String decode(Reader reader, Type type) throws IOException {
throw new IOException("error reading response");
}
};
}
}
TestInterface api = Feign.create(TestInterface.class, server.getUrl("").toString(), new Overrides());
TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(),
new IOEOnDecode());
api.post();
} finally {
@ -445,7 +574,7 @@ public class FeignTest { @@ -445,7 +574,7 @@ public class FeignTest {
}
}
@Module(injects = Client.Default.class, overrides = true)
@Module(injects = Client.Default.class, overrides = true, addsTo = Feign.Defaults.class)
static class TrustSSLSockets {
@Provides SSLSocketFactory trustingSSLSocketFactory() {
return TrustingSSLSocketFactory.get();
@ -454,12 +583,12 @@ public class FeignTest { @@ -454,12 +583,12 @@ public class FeignTest {
@Test public void canOverrideSSLSocketFactory() throws IOException, InterruptedException {
MockWebServer server = new MockWebServer();
server.useHttps(TrustingSSLSocketFactory.get(), false);
server.enqueue(new MockResponse().setResponseCode(200).setBody("success!".getBytes()));
server.useHttps(TrustingSSLSocketFactory.get("localhost"), false);
server.enqueue(new MockResponse().setBody("success!".getBytes()));
server.play();
try {
TestInterface api = Feign.create(TestInterface.class, server.getUrl("").toString(),
TestInterface api = Feign.create(TestInterface.class, "https://localhost:" + server.getPort(),
new TestInterface.Module(), new TrustSSLSockets());
api.post();
} finally {
@ -467,15 +596,37 @@ public class FeignTest { @@ -467,15 +596,37 @@ public class FeignTest {
}
}
@Module(injects = Client.Default.class, overrides = true, addsTo = Feign.Defaults.class)
static class DisableHostnameVerification {
@Provides HostnameVerifier acceptAllHostnameVerifier() {
return new AcceptAllHostnameVerifier();
}
}
@Test public void canOverrideHostnameVerifier() throws IOException, InterruptedException {
MockWebServer server = new MockWebServer();
server.useHttps(TrustingSSLSocketFactory.get("bad.example.com"), false);
server.enqueue(new MockResponse().setBody("success!".getBytes()));
server.play();
try {
TestInterface api = Feign.create(TestInterface.class, "https://localhost:" + server.getPort(),
new TestInterface.Module(), new TrustSSLSockets(), new DisableHostnameVerification());
api.post();
} finally {
server.shutdown();
}
}
@Test public void retriesFailedHandshake() throws IOException, InterruptedException {
MockWebServer server = new MockWebServer();
server.useHttps(TrustingSSLSocketFactory.get(), false);
server.useHttps(TrustingSSLSocketFactory.get("localhost"), false);
server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
server.enqueue(new MockResponse().setResponseCode(200).setBody("success!".getBytes()));
server.enqueue(new MockResponse().setBody("success!".getBytes()));
server.play();
try {
TestInterface api = Feign.create(TestInterface.class, server.getUrl("").toString(),
TestInterface api = Feign.create(TestInterface.class, "https://localhost:" + server.getPort(),
new TestInterface.Module(), new TrustSSLSockets());
api.post();
assertEquals(server.getRequestCount(), 2);
@ -483,4 +634,19 @@ public class FeignTest { @@ -483,4 +634,19 @@ public class FeignTest {
server.shutdown();
}
}
@Test public void equalsAndHashCodeWork() {
TestInterface i1 = Feign.create(TestInterface.class, "http://localhost:8080", new TestInterface.Module());
TestInterface i2 = Feign.create(TestInterface.class, "http://localhost:8080", new TestInterface.Module());
TestInterface i3 = Feign.create(TestInterface.class, "http://localhost:8888", new TestInterface.Module());
OtherTestInterface i4 = Feign.create(OtherTestInterface.class, "http://localhost:8080");
assertTrue(i1.equals(i1));
assertTrue(i1.equals(i2));
assertFalse(i1.equals(i3));
assertFalse(i1.equals(i4));
assertEquals(i1.hashCode(), i1.hashCode());
assertEquals(i1.hashCode(), i2.hashCode());
}
}

41
core/src/test/java/feign/GZIPStreams.java

@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
/*
* Copyright 2013 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package feign;
import com.google.common.io.InputSupplier;
import java.io.IOException;
import java.io.InputStream;
import java.util.zip.GZIPInputStream;
class GZIPStreams {
static InputSupplier<GZIPInputStream> newInputStreamSupplier(InputSupplier<? extends InputStream> supplier) {
return new GZIPInputStreamSupplier(supplier);
}
private static class GZIPInputStreamSupplier implements InputSupplier<GZIPInputStream> {
private final InputSupplier<? extends InputStream> supplier;
GZIPInputStreamSupplier(InputSupplier<? extends InputStream> supplier) {
this.supplier = supplier;
}
@Override
public GZIPInputStream getInput() throws IOException {
return new GZIPInputStream(supplier.getInput());
}
}
}

167
feign-core/src/test/java/feign/LoggerTest.java → core/src/test/java/feign/LoggerTest.java

@ -105,26 +105,9 @@ public class LoggerTest { @@ -105,26 +105,9 @@ public class LoggerTest {
server.enqueue(new MockResponse().setBody("foo"));
server.play();
@dagger.Module(overrides = true, library = true) class Module {
@Provides(type = SET) Encoder defaultEncoder() {
return new Encoder.Text<Object>() {
@Override public String encode(Object object) {
return object.toString();
}
};
}
@Provides @Singleton Logger logger() {
return logger;
}
@Provides @Singleton Logger.Level level() {
return logLevel;
}
}
try {
SendsStuff api = Feign.create(SendsStuff.class, "http://localhost:" + server.getUrl("").getPort(), new Module());
SendsStuff api = Feign.create(SendsStuff.class, "http://localhost:" + server.getUrl("").getPort(),
new DefaultModule(logger, logLevel));
api.login("netflix", "denominator", "password");
@ -140,6 +123,32 @@ public class LoggerTest { @@ -140,6 +123,32 @@ public class LoggerTest {
}
}
static @dagger.Module(overrides = true, library = true) class DefaultModule {
final Logger logger;
final Logger.Level logLevel;
DefaultModule(Logger logger, Logger.Level logLevel) {
this.logger = logger;
this.logLevel = logLevel;
}
@Provides(type = SET) Encoder defaultEncoder() {
return new Encoder.Text<Object>() {
@Override public String encode(Object object) {
return object.toString();
}
};
}
@Provides @Singleton Logger logger() {
return logger;
}
@Provides @Singleton Logger.Level level() {
return logLevel;
}
}
@DataProvider(name = "levelToReadTimeoutOutput")
public Object[][] levelToReadTimeoutOutput() {
Object[][] data = new Object[4][2];
@ -179,36 +188,23 @@ public class LoggerTest { @@ -179,36 +188,23 @@ public class LoggerTest {
return data;
}
@dagger.Module(overrides = true, library = true)
static class LessReadTimeoutModule {
@Provides Request.Options lessReadTimeout() {
return new Request.Options(10 * 1000, 50);
}
}
@Test(dataProvider = "levelToReadTimeoutOutput")
public void readTimeoutEmits(final Logger.Level logLevel, List<String> expectedMessages) throws IOException, InterruptedException {
final MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setBytesPerSecond(1).setBody("foo"));
server.play();
@dagger.Module(overrides = true, library = true) class Module {
@Provides Request.Options lessReadTimeout() {
return new Request.Options(10 * 1000, 50);
}
@Provides(type = SET) Encoder defaultEncoder() {
return new Encoder.Text<Object>() {
@Override public String encode(Object object) {
return object.toString();
}
};
}
@Provides @Singleton Logger logger() {
return logger;
}
@Provides @Singleton Logger.Level level() {
return logLevel;
}
}
try {
SendsStuff api = Feign.create(SendsStuff.class, "http://localhost:" + server.getUrl("").getPort(), new Module());
SendsStuff api = Feign.create(SendsStuff.class, "http://localhost:" + server.getUrl("").getPort(),
new LessReadTimeoutModule(), new DefaultModule(logger, logLevel));
api.login("netflix", "denominator", "password");
@ -257,37 +253,23 @@ public class LoggerTest { @@ -257,37 +253,23 @@ public class LoggerTest {
return data;
}
@dagger.Module(overrides = true, library = true)
static class DontRetryModule {
@Provides Retryer retryer() {
return new Retryer() {
@Override public void continueOrPropagate(RetryableException e) {
throw e;
}
};
}
}
@Test(dataProvider = "levelToUnknownHostOutput")
public void unknownHostEmits(final Logger.Level logLevel, List<String> expectedMessages) throws IOException, InterruptedException {
@dagger.Module(overrides = true, library = true) class Module {
@Provides Retryer retryer() {
return new Retryer() {
@Override public void continueOrPropagate(RetryableException e) {
throw e;
}
};
}
@Provides(type = SET) Encoder defaultEncoder() {
return new Encoder.Text<Object>() {
@Override public String encode(Object object) {
return object.toString();
}
};
}
@Provides @Singleton Logger logger() {
return logger;
}
@Provides @Singleton Logger.Level level() {
return logLevel;
}
}
try {
SendsStuff api = Feign.create(SendsStuff.class, "http://robofu.abc", new Module());
SendsStuff api = Feign.create(SendsStuff.class, "http://robofu.abc",
new DontRetryModule(), new DefaultModule(logger, logLevel));
api.login("netflix", "denominator", "password");
@ -297,43 +279,28 @@ public class LoggerTest { @@ -297,43 +279,28 @@ public class LoggerTest {
}
}
@dagger.Module(overrides = true, library = true)
static class RetryOnceModule {
@Provides Retryer retryer() {
return new Retryer() {
boolean retried;
public void retryEmits() throws IOException, InterruptedException {
@dagger.Module(overrides = true, library = true) class Module {
@Provides Retryer retryer() {
return new Retryer() {
boolean retried;
@Override public void continueOrPropagate(RetryableException e) {
if (!retried) {
retried = true;
return;
}
throw e;
@Override public void continueOrPropagate(RetryableException e) {
if (!retried) {
retried = true;
return;
}
};
}
@Provides(type = SET) Encoder defaultEncoder() {
return new Encoder.Text<Object>() {
@Override public String encode(Object object) {
return object.toString();
}
};
}
@Provides @Singleton Logger logger() {
return logger;
}
@Provides @Singleton Logger.Level level() {
return Logger.Level.BASIC;
}
throw e;
}
};
}
}
public void retryEmits() throws IOException, InterruptedException {
try {
SendsStuff api = Feign.create(SendsStuff.class, "http://robofu.abc", new Module());
SendsStuff api = Feign.create(SendsStuff.class, "http://robofu.abc",
new RetryOnceModule(), new DefaultModule(logger, Logger.Level.BASIC));
api.login("netflix", "denominator", "password");

70
feign-core/src/test/java/feign/RequestTemplateTest.java → core/src/test/java/feign/RequestTemplateTest.java

@ -18,9 +18,10 @@ package feign; @@ -18,9 +18,10 @@ package feign;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import org.testng.annotations.Test;
import java.util.Arrays;
import static feign.RequestTemplate.expand;
import static org.testng.Assert.assertEquals;
@ -84,6 +85,20 @@ public class RequestTemplateTest { @@ -84,6 +85,20 @@ public class RequestTemplateTest {
+ "GET https://iam.amazonaws.com/?Action=DescribeRegions&RegionName.1=eu-west-1 HTTP/1.1\n");
}
@Test public void resolveTemplateWithBaseAndParameterizedIterableQuery() {
RequestTemplate template = new RequestTemplate().method("GET")
.append("/?Query=one").query("Queries", "{queries}");
template.resolve(ImmutableMap.of("queries", Arrays.asList("us-east-1", "eu-west-1")));
assertEquals(template.queries(),
ImmutableListMultimap.<String, String> builder()
.put("Query", "one")
.putAll("Queries", "us-east-1", "eu-west-1")
.build().asMap());
assertEquals(template.toString(), "GET /?Query=one&Queries=us-east-1&Queries=eu-west-1 HTTP/1.1\n");
}
@Test public void resolveTemplateWithMixedRequestLineParams() throws Exception {
RequestTemplate template = new RequestTemplate().method("GET")//
.append("/domains/{domainId}/records")//
@ -107,6 +122,28 @@ public class RequestTemplateTest { @@ -107,6 +122,28 @@ public class RequestTemplateTest {
+ "/domains/1001/records?name=denominator.io&type=CNAME HTTP/1.1\n");
}
@Test public void insertHasQueryParams() throws Exception {
RequestTemplate template = new RequestTemplate().method("GET")//
.append("/domains/{domainId}/records")//
.query("name", "{name}")//
.query("type", "{type}");
template = template.resolve(ImmutableMap.<String, Object>builder()//
.put("domainId", 1001)//
.put("name", "denominator.io")//
.put("type", "CNAME")//
.build()
);
assertEquals(template.toString(), ""//
+ "GET /domains/1001/records?name=denominator.io&type=CNAME HTTP/1.1\n");
template.insert(0, "https://host/v1.0/1234?provider=foo");
assertEquals(template.request().toString(), ""//
+ "GET https://host/v1.0/1234/domains/1001/records?provider=foo&name=denominator.io&type=CNAME HTTP/1.1\n");
}
@Test public void resolveTemplateWithBodyTemplateSetsBodyAndContentLength() {
RequestTemplate template = new RequestTemplate().method("POST")
.bodyTemplate("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", " +
@ -133,4 +170,35 @@ public class RequestTemplateTest { @@ -133,4 +170,35 @@ public class RequestTemplateTest {
+ "\n" //
+ "{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}");
}
@Test public void skipUnresolvedQueries() throws Exception {
RequestTemplate template = new RequestTemplate().method("GET")//
.append("/domains/{domainId}/records")//
.query("optional", "{optional}")//
.query("name", "{nameVariable}");
template = template.resolve(ImmutableMap.<String, Object>builder()//
.put("domainId", 1001)//
.put("nameVariable", "denominator.io")//
.build()
);
assertEquals(template.toString(), ""//
+ "GET /domains/1001/records?name=denominator.io HTTP/1.1\n");
}
@Test public void allQueriesUnresolvable() throws Exception {
RequestTemplate template = new RequestTemplate().method("GET")//
.append("/domains/{domainId}/records")//
.query("optional", "{optional}")//
.query("optional2", "{optional2}");
template = template.resolve(ImmutableMap.<String, Object>builder()//
.put("domainId", 1001)//
.build()
);
assertEquals(template.toString(), ""//
+ "GET /domains/1001/records HTTP/1.1\n");
}
}

193
core/src/test/java/feign/TrustingSSLSocketFactory.java

@ -0,0 +1,193 @@ @@ -0,0 +1,193 @@
/*
* Copyright 2013 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package feign;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.io.Closer;
import com.google.common.io.InputSupplier;
import com.google.common.io.Resources;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.security.KeyStore;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import javax.inject.Provider;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509KeyManager;
import javax.net.ssl.X509TrustManager;
import static com.google.common.base.Throwables.propagate;
/**
* Used for ssl tests to simplify setup.
*/
final class TrustingSSLSocketFactory extends SSLSocketFactory implements X509TrustManager, X509KeyManager {
private static LoadingCache<String, SSLSocketFactory> sslSocketFactories =
CacheBuilder.newBuilder().build(new CacheLoader<String, SSLSocketFactory>() {
@Override
public SSLSocketFactory load(String serverAlias) throws Exception {
return new TrustingSSLSocketFactory(serverAlias);
}
});
public static SSLSocketFactory get() {
return get("");
}
public static SSLSocketFactory get(String serverAlias) {
return sslSocketFactories.getUnchecked(serverAlias);
}
private static final char[] KEYSTORE_PASSWORD = "password".toCharArray();
private final SSLSocketFactory delegate;
private final String serverAlias;
private final PrivateKey privateKey;
private final X509Certificate[] certificateChain;
private TrustingSSLSocketFactory(String serverAlias) {
try {
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(new KeyManager[]{this}, new TrustManager[]{this}, new SecureRandom());
this.delegate = sc.getSocketFactory();
} catch (Exception e) {
throw propagate(e);
}
this.serverAlias = serverAlias;
if (serverAlias.isEmpty()) {
this.privateKey = null;
this.certificateChain = null;
} else {
try {
KeyStore keyStore = loadKeyStore(Resources.newInputStreamSupplier(Resources.getResource("keystore.jks")));
this.privateKey = (PrivateKey) keyStore.getKey(serverAlias, KEYSTORE_PASSWORD);
Certificate[] rawChain = keyStore.getCertificateChain(serverAlias);
this.certificateChain = Arrays.copyOf(rawChain, rawChain.length, X509Certificate[].class);
} catch (Exception e) {
throw propagate(e);
}
}
}
@Override public String[] getDefaultCipherSuites() {
return ENABLED_CIPHER_SUITES;
}
@Override public String[] getSupportedCipherSuites() {
return ENABLED_CIPHER_SUITES;
}
@Override
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
return setEnabledCipherSuites(delegate.createSocket(s, host, port, autoClose));
}
static Socket setEnabledCipherSuites(Socket socket) {
SSLSocket.class.cast(socket).setEnabledCipherSuites(ENABLED_CIPHER_SUITES);
return socket;
}
@Override
public Socket createSocket(String host, int port) throws IOException {
return setEnabledCipherSuites(delegate.createSocket(host, port));
}
@Override public Socket createSocket(InetAddress host, int port) throws IOException {
return setEnabledCipherSuites(delegate.createSocket(host, port));
}
@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
return setEnabledCipherSuites(delegate.createSocket(host, port, localHost, localPort));
}
@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort)
throws IOException {
return setEnabledCipherSuites(delegate.createSocket(address, port, localAddress, localPort));
}
public X509Certificate[] getAcceptedIssuers() {
return null;
}
public void checkClientTrusted(X509Certificate[] certs, String authType) {
}
public void checkServerTrusted(X509Certificate[] certs, String authType) {
}
@Override
public String[] getClientAliases(String keyType, Principal[] issuers) {
return null;
}
@Override
public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
return null;
}
@Override
public String[] getServerAliases(String keyType, Principal[] issuers) {
return null;
}
@Override
public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
return serverAlias;
}
@Override
public X509Certificate[] getCertificateChain(String alias) {
return certificateChain;
}
@Override
public PrivateKey getPrivateKey(String alias) {
return privateKey;
}
private static KeyStore loadKeyStore(InputSupplier<InputStream> inputStreamSupplier) throws IOException {
Closer closer = Closer.create();
try {
InputStream inputStream = closer.register(inputStreamSupplier.getInput());
KeyStore keyStore = KeyStore.getInstance("JKS");
keyStore.load(inputStream, KEYSTORE_PASSWORD);
return keyStore;
} catch (Throwable e) {
throw closer.rethrow(e);
} finally {
closer.close();
}
}
private final static String[] ENABLED_CIPHER_SUITES = {"SSL_RSA_WITH_RC4_128_MD5"};
}

0
feign-core/src/test/java/feign/UtilTest.java → core/src/test/java/feign/UtilTest.java

0
feign-core/src/test/java/feign/codec/DefaultErrorDecoderTest.java → core/src/test/java/feign/codec/DefaultErrorDecoderTest.java

0
feign-core/src/test/java/feign/codec/RetryAfterDecoderTest.java → core/src/test/java/feign/codec/RetryAfterDecoderTest.java

0
feign-core/src/test/java/feign/examples/AWSSignatureVersion4.java → core/src/test/java/feign/examples/AWSSignatureVersion4.java

2
feign-core/src/test/java/feign/examples/GitHubExample.java → core/src/test/java/feign/examples/GitHubExample.java

@ -97,7 +97,7 @@ public class GitHubExample { @@ -97,7 +97,7 @@ public class GitHubExample {
/**
* Here's how it looks to wire json codecs. Note, that you can always instead use {@code feign-gson}!
*/
@Module(overrides = true, library = true)
@Module(library = true)
static class GsonModule {
@Provides @Singleton Gson gson() {

2
feign-core/src/test/java/feign/examples/IAMExample.java → core/src/test/java/feign/examples/IAMExample.java

@ -63,7 +63,7 @@ public class IAMExample { @@ -63,7 +63,7 @@ public class IAMExample {
}
}
@Module(overrides = true, library = true)
@Module(library = true)
static class IAMModule {
@Provides(type = SET) Decoder decoder() {
return Decoders.firstGroup("<Arn>([\\S&&[^<]]+)</Arn>");

BIN
core/src/test/resources/keystore.jks

Binary file not shown.

178
dagger.gradle

@ -0,0 +1,178 @@ @@ -0,0 +1,178 @@
// Manages classpath and IDE annotation processing config for dagger.
//
// setup:
// Add the following to your root build.gradle
//
// apply plugin: 'idea'
// subprojects {
// apply from: rootProject.file('dagger.gradle')
// }
//
// do not use gradle integration of the ide. instead generate and import like so:
//
// ./gradlew clean cleanEclipse cleanIdea eclipse idea
//
// known limitations:
// as output folders include generated classes, you may need to run clean a few times.
// incompatible with android plugin as it applies the java plugin
// unnecessarily applies both eclipse and idea plugins even if you don't use them
// suffers from the normal non-IDE eclipse integration where nested projects don't import properly.
// change your structure to flattened to avoid this.
//
// deprecated by: https://github.com/Netflix/gradle-template/issues/8
//
// original design: cfieber
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
if (!project.hasProperty('daggerVersion')) {
ext {
daggerVersion = "1.1.0"
}
}
configurations {
daggerCompiler {
visible false
}
}
configurations.all {
resolutionStrategy {
eachDependency { DependencyResolveDetails details ->
if (details.requested.group == 'com.squareup.dagger') {
details.useVersion daggerVersion
}
}
}
}
def annotationGeneratedSources = file('.generated/src')
def annotationGeneratedTestSources = file('.generated/test')
task prepareAnnotationGeneratedSourceDirs(overwrite: true) << {
annotationGeneratedSources.mkdirs()
annotationGeneratedTestSources.mkdirs()
sourceSets*.java.srcDirs*.each { it.mkdirs() }
sourceSets*.resources.srcDirs*.each { it.mkdirs() }
}
sourceSets {
main {
java {
compileClasspath += configurations.daggerCompiler
}
}
test {
java {
compileClasspath += configurations.daggerCompiler
}
}
}
dependencies {
compile "com.squareup.dagger:dagger:${project.daggerVersion}"
daggerCompiler "com.squareup.dagger:dagger-compiler:${project.daggerVersion}"
}
rootProject.idea.project.ipr.withXml { projectXml ->
projectXml.asNode().component.find { it.@name == 'CompilerConfiguration' }.annotationProcessing[0].replaceNode {
annotationProcessing {
profile(default: true, name: 'Default', enabled: true) {
sourceOutputDir name: relativePath(annotationGeneratedSources)
sourceTestOutputDir name: relativePath(annotationGeneratedTestSources)
outputRelativeToContentRoot value: true
processorPath useClasspath: true
}
}
}
}
tasks.ideaModule.dependsOn(prepareAnnotationGeneratedSourceDirs)
idea.module {
scopes.PROVIDED.plus += project.configurations.daggerCompiler
iml.withXml { xml->
def moduleSource = xml.asNode().component.find { it.@name = 'NewModuleRootManager' }.content[0]
moduleSource.appendNode('sourceFolder', [url: "file://\$MODULE_DIR\$/${relativePath(annotationGeneratedSources)}", isTestSource: false])
moduleSource.appendNode('sourceFolder', [url: "file://\$MODULE_DIR\$/${relativePath(annotationGeneratedTestSources)}", isTestSource: true])
}
}
tasks.eclipseClasspath.dependsOn(prepareAnnotationGeneratedSourceDirs)
eclipse.classpath {
plusConfigurations += project.configurations.daggerCompiler
}
tasks.eclipseClasspath {
doLast {
eclipse.classpath.file.withXml {
it.asNode().children()[0] + {
classpathentry(kind: 'src', path: relativePath(annotationGeneratedSources)) {
attributes {
attribute name: 'optional', value: true
}
}
}
}
}
}
// http://forums.gradle.org/gradle/topics/eclipse_generated_files_should_be_put_in_the_same_place_as_the_gradle_generated_files
Map pathMappings = [:];
SourceSetContainer sourceSets = project.sourceSets;
sourceSets.each { SourceSet sourceSet ->
String relativeJavaOutputDirectory = project.relativePath(sourceSet.output.classesDir);
String relativeResourceOutputDirectory = project.relativePath(sourceSet.output.resourcesDir);
sourceSet.java.getSrcDirTrees().each { DirectoryTree sourceDirectory ->
String relativeSrcPath = project.relativePath(sourceDirectory.dir.absolutePath);
pathMappings[relativeSrcPath] = relativeJavaOutputDirectory;
}
sourceSet.resources.getSrcDirTrees().each { DirectoryTree resourceDirectory ->
String relativeResourcePath = project.relativePath(resourceDirectory.dir.absolutePath);
pathMappings[relativeResourcePath] = relativeResourceOutputDirectory;
}
}
project.eclipse.classpath.file {
whenMerged { classpath ->
classpath.entries.findAll { entry ->
return entry.kind == 'src';
}.each { entry ->
if(pathMappings.containsKey(entry.path)) {
entry.output = pathMappings[entry.path];
}
}
}
}
eclipse.jdt.file.withProperties { props ->
props.setProperty('org.eclipse.jdt.core.compiler.processAnnotations', 'enabled')
}
tasks.eclipseJdt {
doFirst {
def aptPrefs = file('.settings/org.eclipse.jdt.apt.core.prefs')
aptPrefs.parentFile.mkdirs()
aptPrefs.text = """\
eclipse.preferences.version=1
org.eclipse.jdt.apt.aptEnabled=true
org.eclipse.jdt.apt.genSrcDir=${relativePath(annotationGeneratedSources)}
org.eclipse.jdt.apt.reconcileEnabled=true
""".stripIndent()
file('.factorypath').withWriter {
new groovy.xml.MarkupBuilder(it).'factorypath' {
project.configurations.daggerCompiler.files.each { dep ->
'factorypathentry' kind: 'EXTJAR', id: dep.absolutePath, enabled: true, runInBatchMode: false
}
}
}
}
}

8
examples/feign-example-cli/build.gradle → example-github/build.gradle

@ -1,9 +1,9 @@ @@ -1,9 +1,9 @@
apply plugin: 'java'
dependencies {
compile 'com.netflix.feign:feign-core:3.0.0'
compile 'com.netflix.feign:feign-gson:3.0.0'
provided 'com.squareup.dagger:dagger-compiler:1.0.1'
compile 'com.netflix.feign:feign-core:4.1.0'
compile 'com.netflix.feign:feign-gson:4.1.0'
provided 'com.squareup.dagger:dagger-compiler:1.1.0'
}
// create a self-contained jar that is executable
@ -26,7 +26,7 @@ task fatJar(dependsOn: classes, type: Jar) { @@ -26,7 +26,7 @@ task fatJar(dependsOn: classes, type: Jar) {
// http://skife.org/java/unix/2011/06/20/really_executable_jars.html
manifest {
attributes 'Main-Class': 'feign.example.cli.GitHubExample'
attributes 'Main-Class': 'feign.example.github.GitHubExample'
}
// for convenience, we make a file in the build dir named github with no extension

78
examples/feign-example-cli/src/main/java/feign/example/cli/GitHubExample.java → example-github/src/main/java/feign/example/github/GitHubExample.java

@ -13,10 +13,14 @@ @@ -13,10 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package feign.example.cli;
package feign.example.github;
import dagger.Module;
import dagger.Provides;
import feign.Feign;
import feign.IncrementalCallback;
import feign.Logger;
import feign.Observable;
import feign.Observer;
import feign.RequestLine;
import feign.gson.GsonModule;
@ -34,8 +38,7 @@ public class GitHubExample { @@ -34,8 +38,7 @@ public class GitHubExample {
List<Contributor> contributors(@Named("owner") String owner, @Named("repo") String repo);
@RequestLine("GET /repos/{owner}/{repo}/contributors")
void contributors(@Named("owner") String owner, @Named("repo") String repo,
IncrementalCallback<Contributor> contributors);
Observable<Contributor> observable(@Named("owner") String owner, @Named("repo") String repo);
}
static class Contributor {
@ -44,7 +47,7 @@ public class GitHubExample { @@ -44,7 +47,7 @@ public class GitHubExample {
}
public static void main(String... args) throws InterruptedException {
GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule());
GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GitHubModule());
System.out.println("Let's fetch and print a list of the contributors to this library.");
List<Contributor> contributors = github.contributors("netflix", "feign");
@ -52,36 +55,55 @@ public class GitHubExample { @@ -52,36 +55,55 @@ public class GitHubExample {
System.out.println(contributor.login + " (" + contributor.contributions + ")");
}
final CountDownLatch latch = new CountDownLatch(1);
System.out.println("Let's treat our contributors as an observable.");
Observable<Contributor> observable = github.observable("netflix", "feign");
System.out.println("Now, let's do it as an incremental async task.");
IncrementalCallback<Contributor> task = new IncrementalCallback<Contributor>() {
CountDownLatch latch = new CountDownLatch(2);
public int count;
System.out.println("Let's add 2 subscribers.");
observable.subscribe(new ContributorObserver(latch));
observable.subscribe(new ContributorObserver(latch));
// parsed directly from the text stream without an intermediate collection.
@Override public void onNext(Contributor contributor) {
System.out.println(contributor.login + " (" + contributor.contributions + ")");
count++;
}
// wait for the task to complete.
latch.await();
@Override public void onSuccess() {
System.out.println("found " + count + " contributors");
latch.countDown();
}
System.exit(0);
}
@Override public void onFailure(Throwable cause) {
cause.printStackTrace();
latch.countDown();
}
};
static class ContributorObserver implements Observer<Contributor> {
// fire a task in the background.
github.contributors("netflix", "feign", task);
private final CountDownLatch latch;
public int count;
// wait for the task to complete.
latch.await();
public ContributorObserver(CountDownLatch latch) {
this.latch = latch;
}
System.exit(0);
// parsed directly from the text stream without an intermediate collection.
@Override public void onNext(Contributor contributor) {
count++;
}
@Override public void onSuccess() {
System.out.println("found " + count + " contributors");
latch.countDown();
}
@Override public void onFailure(Throwable cause) {
cause.printStackTrace();
latch.countDown();
}
}
@Module(overrides = true, library = true, includes = GsonModule.class)
static class GitHubModule {
@Provides Logger.Level loggingLevel() {
return Logger.Level.BASIC;
}
@Provides Logger logger() {
return new Logger.ErrorLogger();
}
}
}

49
example-wikipedia/build.gradle

@ -0,0 +1,49 @@ @@ -0,0 +1,49 @@
apply plugin: 'java'
dependencies {
compile 'com.netflix.feign:feign-core:4.1.0'
compile 'com.netflix.feign:feign-gson:4.1.0'
provided 'com.squareup.dagger:dagger-compiler:1.1.0'
}
// create a self-contained jar that is executable
// the output is both a 'fat' project artifact and
// a convenience file named "build/github"
task fatJar(dependsOn: classes, type: Jar) {
classifier 'fat'
doFirst {
// Delay evaluation until the compile configuration is ready
from {
configurations.compile.collect { zipTree(it) }
}
}
from (sourceSets*.output.classesDir) {
}
// really executable jar
// http://skife.org/java/unix/2011/06/20/really_executable_jars.html
manifest {
attributes 'Main-Class': 'feign.example.wikipedia.WikipediaExample'
}
// for convenience, we make a file in the build dir named github with no extension
doLast {
def srcFile = new File("${buildDir}/libs/${archiveName}")
def shortcutFile = new File("${buildDir}/wikipedia")
shortcutFile.delete()
shortcutFile << "#!/usr/bin/env sh\n"
shortcutFile << 'exec java -jar $0 "$@"' + "\n"
shortcutFile << srcFile.bytes
shortcutFile.setExecutable(true, true)
srcFile.delete()
srcFile << shortcutFile.bytes
srcFile.setExecutable(true, true)
}
}
artifacts {
archives fatJar
}

87
example-wikipedia/src/main/java/feign/example/wikipedia/ResponseDecoder.java

@ -0,0 +1,87 @@ @@ -0,0 +1,87 @@
package feign.example.wikipedia;
import com.google.gson.stream.JsonReader;
import feign.codec.Decoder;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.Type;
abstract class ResponseDecoder<X> implements Decoder.TextStream<WikipediaExample.Response<X>> {
/**
* name of the key inside the {@code query} dict which holds the elements desired. ex. {@code pages}.
*/
protected abstract String query();
/**
* Parses the contents of a result object.
* <p/>
* <br>
* ex. If {@link #query()} is {@code pages}, then this would parse the value of each key in the dict {@code pages}.
* In the example below, this would first start at line {@code 3}.
* <p/>
* <pre>
* "pages": {
* "2576129": {
* "pageid": 2576129,
* "title": "Burchell's zebra",
* --snip--
* </pre>
*/
protected abstract X build(JsonReader reader) throws IOException;
/**
* the wikipedia api doesn't use json arrays, rather a series of nested objects.
*/
@Override
public WikipediaExample.Response<X> decode(Reader ireader, Type type) throws IOException {
WikipediaExample.Response<X> pages = new WikipediaExample.Response<X>();
JsonReader reader = new JsonReader(ireader);
reader.beginObject();
while (reader.hasNext()) {
String nextName = reader.nextName();
if ("query".equals(nextName)) {
reader.beginObject();
while (reader.hasNext()) {
if (query().equals(reader.nextName())) {
reader.beginObject();
while (reader.hasNext()) {
// each element is in form: "id" : { object }
// this advances the pointer to the value and skips the key
reader.nextName();
reader.beginObject();
pages.add(build(reader));
reader.endObject();
}
reader.endObject();
} else {
reader.skipValue();
}
}
reader.endObject();
} else if ("query-continue".equals(nextName)) {
reader.beginObject();
while (reader.hasNext()) {
if ("search".equals(reader.nextName())) {
reader.beginObject();
while (reader.hasNext()) {
if ("gsroffset".equals(reader.nextName())) {
pages.nextOffset = reader.nextLong();
}
}
reader.endObject();
} else {
reader.skipValue();
}
}
reader.endObject();
} else {
reader.skipValue();
}
}
reader.endObject();
reader.close();
return pages;
}
}

145
example-wikipedia/src/main/java/feign/example/wikipedia/WikipediaExample.java

@ -0,0 +1,145 @@ @@ -0,0 +1,145 @@
/*
* Copyright 2013 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package feign.example.wikipedia;
import com.google.gson.stream.JsonReader;
import dagger.Module;
import dagger.Provides;
import feign.Feign;
import feign.Logger;
import feign.RequestLine;
import feign.codec.Decoder;
import feign.gson.GsonModule;
import javax.inject.Named;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import static dagger.Provides.Type.SET;
import static feign.Logger.ErrorLogger;
import static feign.Logger.Level.BASIC;
public class WikipediaExample {
public static interface Wikipedia {
@RequestLine("GET /w/api.php?action=query&generator=search&prop=info&format=json&gsrsearch={search}")
Response<Page> search(@Named("search") String search);
@RequestLine("GET /w/api.php?action=query&generator=search&prop=info&format=json&gsrsearch={search}&gsroffset={offset}")
Response<Page> resumeSearch(@Named("search") String search, @Named("offset") long offset);
}
static class Page {
long id;
String title;
}
public static class Response<X> extends ArrayList<X> {
/**
* when present, the position to resume the list.
*/
Long nextOffset;
}
public static void main(String... args) throws InterruptedException {
Wikipedia wikipedia = Feign.create(Wikipedia.class, "http://en.wikipedia.org", new WikipediaModule());
System.out.println("Let's search for PTAL!");
Iterator<Page> pages = lazySearch(wikipedia, "PTAL");
while (pages.hasNext()) {
System.out.println(pages.next().title);
}
}
/**
* this will lazily continue searches, making new http calls as necessary.
*
* @param wikipedia used to search
* @param query see {@link Wikipedia#search(String)}.
*/
static Iterator<Page> lazySearch(final Wikipedia wikipedia, final String query) {
final Response<Page> first = wikipedia.search(query);
if (first.nextOffset == null)
return first.iterator();
return new Iterator<Page>() {
Iterator<Page> current = first.iterator();
Long nextOffset = first.nextOffset;
@Override
public boolean hasNext() {
while (!current.hasNext() && nextOffset != null) {
System.out.println("Wow.. even more results than " + nextOffset);
Response<Page> nextPage = wikipedia.resumeSearch(query, nextOffset);
current = nextPage.iterator();
nextOffset = nextPage.nextOffset;
}
return current.hasNext();
}
@Override
public Page next() {
return current.next();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
@Module(overrides = true, library = true, includes = GsonModule.class)
static class WikipediaModule {
@Provides Logger.Level loggingLevel() {
return BASIC;
}
@Provides Logger logger() {
return new ErrorLogger();
}
/**
* add to the set of Decoders one that handles {@code Response<Page>}.
*/
@Provides(type = SET) Decoder pagesDecoder() {
return new ResponseDecoder<Page>() {
@Override
protected String query() {
return "pages";
}
@Override
protected Page build(JsonReader reader) throws IOException {
Page page = new Page();
while (reader.hasNext()) {
String key = reader.nextName();
if (key.equals("pageid")) {
page.id = reader.nextLong();
} else if (key.equals("title")) {
page.title = reader.nextString();
} else {
reader.skipValue();
}
}
return page;
}
};
}
}
}

114
feign-core/src/test/java/feign/TrustingSSLSocketFactory.java

@ -1,114 +0,0 @@ @@ -1,114 +0,0 @@
/*
* Copyright 2013 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package feign;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import javax.inject.Provider;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import static com.google.common.base.Throwables.propagate;
/**
* used for ssl tests so that they can avoid having to read a keystore.
*/
final class TrustingSSLSocketFactory extends SSLSocketFactory implements X509TrustManager, KeyManager {
public static SSLSocketFactory get() {
return Singleton.INSTANCE.get();
}
private final SSLSocketFactory delegate;
private TrustingSSLSocketFactory() {
try {
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(new KeyManager[]{this}, new TrustManager[]{this}, new SecureRandom());
this.delegate = sc.getSocketFactory();
} catch (Exception e) {
throw propagate(e);
}
}
@Override public String[] getDefaultCipherSuites() {
return ENABLED_CIPHER_SUITES;
}
@Override public String[] getSupportedCipherSuites() {
return ENABLED_CIPHER_SUITES;
}
@Override
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
return setEnabledCipherSuites(delegate.createSocket(s, host, port, autoClose));
}
static Socket setEnabledCipherSuites(Socket socket) {
SSLSocket.class.cast(socket).setEnabledCipherSuites(ENABLED_CIPHER_SUITES);
return socket;
}
@Override
public Socket createSocket(String host, int port) throws IOException {
return setEnabledCipherSuites(delegate.createSocket(host, port));
}
@Override public Socket createSocket(InetAddress host, int port) throws IOException {
return setEnabledCipherSuites(delegate.createSocket(host, port));
}
@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
return setEnabledCipherSuites(delegate.createSocket(host, port, localHost, localPort));
}
@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort)
throws IOException {
return setEnabledCipherSuites(delegate.createSocket(address, port, localAddress, localPort));
}
public X509Certificate[] getAcceptedIssuers() {
return null;
}
public void checkClientTrusted(X509Certificate[] certs, String authType) {
}
public void checkServerTrusted(X509Certificate[] certs, String authType) {
}
private final static String[] ENABLED_CIPHER_SUITES = {"SSL_DH_anon_WITH_RC4_128_MD5"};
private static enum Singleton implements Provider<SSLSocketFactory> {
INSTANCE;
private final SSLSocketFactory sslSocketFactory = new TrustingSSLSocketFactory();
@Override public SSLSocketFactory get() {
return sslSocketFactory;
}
}
}

76
feign-jaxrs/src/test/java/feign/jaxrs/examples/IAMExample.java

@ -1,76 +0,0 @@ @@ -1,76 +0,0 @@
/*
* Copyright 2013 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package feign.jaxrs.examples;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import dagger.Module;
import dagger.Provides;
import feign.Feign;
import feign.Request;
import feign.RequestTemplate;
import feign.Target;
import feign.codec.Decoder;
import feign.codec.Decoders;
import feign.examples.AWSSignatureVersion4;
import feign.jaxrs.JAXRSModule;
import static dagger.Provides.Type.SET;
public class IAMExample {
interface IAM {
@GET @Path("/?Action=GetUser&Version=2010-05-08") String arn();
}
public static void main(String... args) {
IAM iam = Feign.create(new IAMTarget(args[0], args[1]), new IAMModule());
System.out.println(iam.arn());
}
static class IAMTarget extends AWSSignatureVersion4 implements Target<IAM> {
@Override public Class<IAM> type() {
return IAM.class;
}
@Override public String name() {
return "iam";
}
@Override public String url() {
return "https://iam.amazonaws.com";
}
private IAMTarget(String accessKey, String secretKey) {
super(accessKey, secretKey);
}
@Override public Request apply(RequestTemplate in) {
in.insert(0, url());
return super.apply(in);
}
}
@Module(overrides = true, library = true, includes = JAXRSModule.class)
static class IAMModule {
@Provides(type = SET) Decoder decoder() {
return Decoders.firstGroup("<Arn>([\\S&&[^<]]+)</Arn>");
}
}
}

2
gradle.properties

@ -1 +1 @@ @@ -1 +1 @@
version=4.0.0-SNAPSHOT
version=4.4.1-SNAPSHOT

0
feign-gson/README.md → gson/README.md

2
feign-gson/src/main/java/feign/gson/GsonModule.java → gson/src/main/java/feign/gson/GsonModule.java

@ -43,7 +43,7 @@ import java.util.concurrent.atomic.AtomicBoolean; @@ -43,7 +43,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
import static dagger.Provides.Type.SET;
@dagger.Module(library = true, overrides = true)
@dagger.Module(library = true)
public final class GsonModule {
@Provides(type = SET) Encoder encoder(GsonCodec codec) {

52
feign-gson/src/test/java/feign/gson/GsonModuleTest.java → gson/src/test/java/feign/gson/GsonModuleTest.java

@ -40,15 +40,15 @@ import static org.testng.Assert.fail; @@ -40,15 +40,15 @@ import static org.testng.Assert.fail;
@Test
public class GsonModuleTest {
@Module(includes = GsonModule.class, library = true, injects = EncodersAndDecoders.class)
static class EncodersAndDecoders {
@Inject Set<Encoder> encoders;
@Inject Set<Decoder> decoders;
@Inject Set<IncrementalDecoder> incrementalDecoders;
}
@Test public void providesEncoderDecoderAndIncrementalDecoder() throws Exception {
@Module(includes = GsonModule.class, injects = SetBindings.class) class SetBindings {
@Inject Set<Encoder> encoders;
@Inject Set<Decoder> decoders;
@Inject Set<IncrementalDecoder> incrementalDecoders;
}
SetBindings bindings = new SetBindings();
EncodersAndDecoders bindings = new EncodersAndDecoders();
ObjectGraph.create(bindings).inject(bindings);
assertEquals(bindings.encoders.size(), 1);
@ -59,12 +59,13 @@ public class GsonModuleTest { @@ -59,12 +59,13 @@ public class GsonModuleTest {
assertEquals(bindings.incrementalDecoders.iterator().next().getClass(), GsonModule.GsonCodec.class);
}
@Test public void encodesMapObjectNumericalValuesAsInteger() throws Exception {
@Module(includes = GsonModule.class, injects = SetBindings.class) class SetBindings {
@Inject Set<Encoder> encoders;
}
@Module(includes = GsonModule.class, library = true, injects = Encoders.class)
static class Encoders {
@Inject Set<Encoder> encoders;
}
SetBindings bindings = new SetBindings();
@Test public void encodesMapObjectNumericalValuesAsInteger() throws Exception {
Encoders bindings = new Encoders();
ObjectGraph.create(bindings).inject(bindings);
Map<String, Object> map = new LinkedHashMap<String, Object>();
@ -77,11 +78,8 @@ public class GsonModuleTest { @@ -77,11 +78,8 @@ public class GsonModuleTest {
}
@Test public void encodesFormParams() throws Exception {
@Module(includes = GsonModule.class, injects = SetBindings.class) class SetBindings {
@Inject Set<Encoder> encoders;
}
SetBindings bindings = new SetBindings();
Encoders bindings = new Encoders();
ObjectGraph.create(bindings).inject(bindings);
Map<String, Object> form = new LinkedHashMap<String, Object>();
@ -116,12 +114,13 @@ public class GsonModuleTest { @@ -116,12 +114,13 @@ public class GsonModuleTest {
private static final long serialVersionUID = 1L;
}
@Test public void decodes() throws Exception {
@Module(includes = GsonModule.class, injects = SetBindings.class) class SetBindings {
@Inject Set<Decoder> decoders;
}
@Module(includes = GsonModule.class, library = true, injects = Decoders.class)
static class Decoders {
@Inject Set<Decoder> decoders;
}
SetBindings bindings = new SetBindings();
@Test public void decodes() throws Exception {
Decoders bindings = new Decoders();
ObjectGraph.create(bindings).inject(bindings);
List<Zone> zones = new LinkedList<Zone>();
@ -133,12 +132,13 @@ public class GsonModuleTest { @@ -133,12 +132,13 @@ public class GsonModuleTest {
}.getType()), zones);
}
@Test public void decodesIncrementally() throws Exception {
@Module(includes = GsonModule.class, injects = SetBindings.class) class SetBindings {
@Inject Set<IncrementalDecoder> decoders;
}
@Module(includes = GsonModule.class, library = true, injects = IncrementalDecoders.class)
static class IncrementalDecoders {
@Inject Set<IncrementalDecoder> decoders;
}
SetBindings bindings = new SetBindings();
@Test public void decodesIncrementally() throws Exception {
IncrementalDecoders bindings = new IncrementalDecoders();
ObjectGraph.create(bindings).inject(bindings);
final List<Zone> zones = new LinkedList<Zone>();

37
jaxrs/README.md

@ -0,0 +1,37 @@ @@ -0,0 +1,37 @@
# Feign JAXRS
This module overrides annotation processing to instead use standard ones supplied by the JAX-RS specification. This is currently targeted at the 1.1 spec.
## Limitations
While it may appear possible to reuse the same interface across client and server, bear in mind that JAX-RS resource
annotations were not designed to be processed by clients. Moreover, JAX-RS 2.0 has a different package hierarchy for
client invocation. Finally, JAX-RS is a large spec and attempts to implement it completely would be a project larger
than feign itself. In other words, this implementation is *best efforts* and concedes far from 100% compatibility with
server interface behavior.
## Currently Supported Annotation Processing
Feign only supports processing java interfaces (not abstract or concrete classes).
ISE is raised when any annotation's value is empty or null. Ex. `Path("")` raises an ISE.
Here are a list of behaviors currently supported.
### Type Annotations
#### `@Path`
Appends the value to `Target.url()`. Can have tokens corresponding to `@PathParam` annotations.
### Method Annotations
#### `@HttpMethod` meta-annotation (present on `@GET`, `@POST`, etc.)
Sets the request method.
#### `@Path`
Appends the value to `Target.url()`. Can have tokens corresponding to `@PathParam` annotations.
#### `@Produces`
Adds the first value as the `Accept` header.
#### `@Consumes`
Adds the first value as the `Content-Type` header.
### Parameter Annotations
#### `@PathParam`
Links the value of the corresponding parameter to a template variable declared in the path.
#### `@QueryParam`
Links the value of the corresponding parameter to a query parameter. When invoked, null will skip the query param.
#### `@HeaderParam`
Links the value of the corresponding parameter to a header.
#### `@FormParam`
Links the value of the corresponding parameter to a key passed to `Encoder.Text<Map<String, Object>>.encode()`.

53
feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java → jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java

@ -33,7 +33,12 @@ import java.lang.reflect.Method; @@ -33,7 +33,12 @@ import java.lang.reflect.Method;
import java.util.Collection;
import static feign.Util.checkState;
import static feign.Util.emptyToNull;
/**
* Please refer to the
* <a href="https://github.com/Netflix/feign/tree/master/feign-jaxrs">Feign JAX-RS README</a>.
*/
@dagger.Module(library = true, overrides = true)
public final class JAXRSModule {
static final String ACCEPT = "Accept";
@ -45,6 +50,18 @@ public final class JAXRSModule { @@ -45,6 +50,18 @@ public final class JAXRSModule {
public static final class JAXRSContract extends Contract.BaseContract {
@Override
public MethodMetadata parseAndValidatateMetadata(Method method) {
MethodMetadata md = super.parseAndValidatateMetadata(method);
Path path = method.getDeclaringClass().getAnnotation(Path.class);
if (path != null) {
String pathValue = emptyToNull(path.value());
checkState(pathValue != null, "Path.value() was empty on type %s", method.getDeclaringClass().getName());
md.template().insert(0, pathValue);
}
return md;
}
@Override
protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) {
Class<? extends Annotation> annotationType = methodAnnotation.annotationType();
@ -54,19 +71,20 @@ public final class JAXRSModule { @@ -54,19 +71,20 @@ public final class JAXRSModule {
"Method %s contains multiple HTTP methods. Found: %s and %s", method.getName(), data.template()
.method(), http.value());
data.template().method(http.value());
} else if (annotationType == Body.class) {
String body = Body.class.cast(methodAnnotation).value();
if (body.indexOf('{') == -1) {
data.template().body(body);
} else {
data.template().bodyTemplate(body);
}
} else if (annotationType == Path.class) {
String pathValue = emptyToNull(Path.class.cast(methodAnnotation).value());
checkState(pathValue != null, "Path.value() was empty on method %s", method.getName());
data.template().append(Path.class.cast(methodAnnotation).value());
} else if (annotationType == Produces.class) {
data.template().header(CONTENT_TYPE, join(',', ((Produces) methodAnnotation).value()));
String[] serverProduces = ((Produces) methodAnnotation).value();
String clientAccepts = serverProduces.length == 0 ? null: emptyToNull(serverProduces[0]);
checkState(clientAccepts != null, "Produces.value() was empty on method %s", method.getName());
data.template().header(ACCEPT, clientAccepts);
} else if (annotationType == Consumes.class) {
data.template().header(ACCEPT, join(',', ((Consumes) methodAnnotation).value()));
String[] serverConsumes = ((Consumes) methodAnnotation).value();
String clientProduces = serverConsumes.length == 0 ? null: emptyToNull(serverConsumes[0]);
checkState(clientProduces != null, "Consumes.value() was empty on method %s", method.getName());
data.template().header(CONTENT_TYPE, clientProduces);
}
}
@ -77,22 +95,26 @@ public final class JAXRSModule { @@ -77,22 +95,26 @@ public final class JAXRSModule {
Class<? extends Annotation> annotationType = parameterAnnotation.annotationType();
if (annotationType == PathParam.class) {
String name = PathParam.class.cast(parameterAnnotation).value();
checkState(emptyToNull(name) != null, "PathParam.value() was empty on parameter %s", paramIndex);
nameParam(data, name, paramIndex);
isHttpParam = true;
} else if (annotationType == QueryParam.class) {
String name = QueryParam.class.cast(parameterAnnotation).value();
checkState(emptyToNull(name) != null, "QueryParam.value() was empty on parameter %s", paramIndex);
Collection<String> query = addTemplatedParam(data.template().queries().get(name), name);
data.template().query(name, query);
nameParam(data, name, paramIndex);
isHttpParam = true;
} else if (annotationType == HeaderParam.class) {
String name = HeaderParam.class.cast(parameterAnnotation).value();
checkState(emptyToNull(name) != null, "HeaderParam.value() was empty on parameter %s", paramIndex);
Collection<String> header = addTemplatedParam(data.template().headers().get(name), name);
data.template().header(name, header);
nameParam(data, name, paramIndex);
isHttpParam = true;
} else if (annotationType == FormParam.class) {
String name = FormParam.class.cast(parameterAnnotation).value();
checkState(emptyToNull(name) != null, "FormParam.value() was empty on parameter %s", paramIndex);
data.formParams().add(name);
nameParam(data, name, paramIndex);
isHttpParam = true;
@ -101,17 +123,4 @@ public final class JAXRSModule { @@ -101,17 +123,4 @@ public final class JAXRSModule {
return isHttpParam;
}
}
private static String join(char separator, String... parts) {
if (parts == null || parts.length == 0)
return "";
StringBuilder to = new StringBuilder();
for (int i = 0; i < parts.length; i++) {
to.append(parts[i]);
if (i + 1 < parts.length) {
to.append(separator);
}
}
return to.toString();
}
}

120
feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java → jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java

@ -18,13 +18,13 @@ package feign.jaxrs; @@ -18,13 +18,13 @@ package feign.jaxrs;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.gson.reflect.TypeToken;
import feign.Body;
import feign.MethodMetadata;
import feign.Observable;
import feign.Observer;
import feign.Response;
import org.testng.annotations.Test;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
@ -44,14 +44,15 @@ import java.lang.reflect.Type; @@ -44,14 +44,15 @@ import java.lang.reflect.Type;
import java.net.URI;
import java.util.List;
import static feign.jaxrs.JAXRSModule.ACCEPT;
import static feign.jaxrs.JAXRSModule.CONTENT_TYPE;
import static javax.ws.rs.HttpMethod.DELETE;
import static javax.ws.rs.HttpMethod.GET;
import static javax.ws.rs.HttpMethod.POST;
import static javax.ws.rs.HttpMethod.PUT;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static javax.ws.rs.core.MediaType.APPLICATION_XML;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
@ -154,21 +155,48 @@ public class JAXRSContractTest { @@ -154,21 +155,48 @@ public class JAXRSContractTest {
}
}
interface BodyWithoutParameters {
@POST @Produces(APPLICATION_XML) @Body("<v01:getAccountsListOfUser/>") Response post();
interface ProducesAndConsumes {
@GET @Produces(APPLICATION_XML) Response produces();
@GET @Produces({}) Response producesNada();
@GET @Produces({""}) Response producesEmpty();
@POST @Consumes(APPLICATION_JSON) Response consumes();
@POST @Consumes({}) Response consumesNada();
@POST @Consumes({""}) Response consumesEmpty();
}
@Test public void producesAddsAcceptHeader() throws Exception {
MethodMetadata md = contract.parseAndValidatateMetadata(ProducesAndConsumes.class.getDeclaredMethod("produces"));
assertEquals(md.template().headers().get(ACCEPT), ImmutableSet.of(APPLICATION_XML));
}
@Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "Produces.value\\(\\) was empty on method producesNada")
public void producesNada() throws Exception {
contract.parseAndValidatateMetadata(ProducesAndConsumes.class.getDeclaredMethod("producesNada"));
}
@Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "Produces.value\\(\\) was empty on method producesEmpty")
public void producesEmpty() throws Exception {
contract.parseAndValidatateMetadata(ProducesAndConsumes.class.getDeclaredMethod("producesEmpty"));
}
@Test public void consumesAddsContentTypeHeader() throws Exception {
MethodMetadata md = contract.parseAndValidatateMetadata(ProducesAndConsumes.class.getDeclaredMethod("consumes"));
assertEquals(md.template().headers().get(CONTENT_TYPE), ImmutableSet.of(APPLICATION_JSON));
}
@Test public void bodyWithoutParameters() throws Exception {
MethodMetadata md = contract.parseAndValidatateMetadata(BodyWithoutParameters.class.getDeclaredMethod("post"));
assertEquals(md.template().body(), "<v01:getAccountsListOfUser/>");
assertFalse(md.template().bodyTemplate() != null);
assertTrue(md.formParams().isEmpty());
assertTrue(md.indexToName().isEmpty());
@Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "Consumes.value\\(\\) was empty on method consumesNada")
public void consumesNada() throws Exception {
contract.parseAndValidatateMetadata(ProducesAndConsumes.class.getDeclaredMethod("consumesNada"));
}
@Test public void producesAddsContentTypeHeader() throws Exception {
MethodMetadata md = contract.parseAndValidatateMetadata(BodyWithoutParameters.class.getDeclaredMethod("post"));
assertEquals(md.template().headers().get(CONTENT_TYPE), ImmutableSet.of(APPLICATION_XML));
@Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "Consumes.value\\(\\) was empty on method consumesEmpty")
public void consumesEmpty() throws Exception {
contract.parseAndValidatateMetadata(ProducesAndConsumes.class.getDeclaredMethod("consumesEmpty"));
}
interface BodyParams {
@ -193,6 +221,42 @@ public class JAXRSContractTest { @@ -193,6 +221,42 @@ public class JAXRSContractTest {
contract.parseAndValidatateMetadata(BodyParams.class.getDeclaredMethod("tooMany", List.class, List.class));
}
@Path("") interface EmptyPathOnType {
@GET Response base();
}
@Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "Path.value\\(\\) was empty on type .*")
public void emptyPathOnType() throws Exception {
contract.parseAndValidatateMetadata(EmptyPathOnType.class.getDeclaredMethod("base"));
}
@Path("/base") interface PathOnType {
@GET Response base();
@GET @Path("/specific") Response get();
@GET @Path("") Response emptyPath();
@GET @Path("/{param}") Response emptyPathParam(@PathParam("") String empty);
}
@Test public void pathOnType() throws Exception {
MethodMetadata md = contract.parseAndValidatateMetadata(PathOnType.class.getDeclaredMethod("base"));
assertEquals(md.template().url(), "/base");
md = contract.parseAndValidatateMetadata(PathOnType.class.getDeclaredMethod("get"));
assertEquals(md.template().url(), "/base/specific");
}
@Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "Path.value\\(\\) was empty on method emptyPath")
public void emptyPathOnMethod() throws Exception {
contract.parseAndValidatateMetadata(PathOnType.class.getDeclaredMethod("emptyPath"));
}
@Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "PathParam.value\\(\\) was empty on parameter 0")
public void emptyPathParam() throws Exception {
contract.parseAndValidatateMetadata(PathOnType.class.getDeclaredMethod("emptyPathParam", String.class));
}
interface WithURIParam {
@GET @Path("/{1}/{2}") Response uriParam(@PathParam("1") String one, URI endpoint, @PathParam("2") String two);
}
@ -215,6 +279,8 @@ public class JAXRSContractTest { @@ -215,6 +279,8 @@ public class JAXRSContractTest {
@GET @Path("/domains/{domainId}/records")
Response recordsByNameAndType(@PathParam("domainId") int id, @QueryParam("name") String nameFilter,
@QueryParam("type") String typeFilter);
@GET Response emptyQueryParam(@QueryParam("") String empty);
}
@Test public void mixedRequestLineParams() throws Exception {
@ -232,29 +298,40 @@ public class JAXRSContractTest { @@ -232,29 +298,40 @@ public class JAXRSContractTest {
assertEquals(md.template().toString(), "GET /domains/{domainId}/records?name={name}&type={type} HTTP/1.1\n");
}
@Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "QueryParam.value\\(\\) was empty on parameter 0")
public void emptyQueryParam() throws Exception {
contract.parseAndValidatateMetadata(WithPathAndQueryParams.class.getDeclaredMethod("emptyQueryParam", String.class));
}
interface FormParams {
@POST
@Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D")
void login(
@POST void login(
@FormParam("customer_name") String customer,
@FormParam("user_name") String user, @FormParam("password") String password);
@GET Response emptyFormParam(@FormParam("") String empty);
}
@Test public void formParamsParseIntoIndexToName() throws Exception {
MethodMetadata md = contract.parseAndValidatateMetadata(FormParams.class.getDeclaredMethod("login", String.class,
String.class, String.class));
assertFalse(md.template().body() != null);
assertEquals(md.template().bodyTemplate(),
"%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D");
assertNull(md.template().body());
assertNull(md.template().bodyTemplate());
assertEquals(md.formParams(), ImmutableList.of("customer_name", "user_name", "password"));
assertEquals(md.indexToName().get(0), ImmutableSet.of("customer_name"));
assertEquals(md.indexToName().get(1), ImmutableSet.of("user_name"));
assertEquals(md.indexToName().get(2), ImmutableSet.of("password"));
}
@Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "FormParam.value\\(\\) was empty on parameter 0")
public void emptyFormParam() throws Exception {
contract.parseAndValidatateMetadata(FormParams.class.getDeclaredMethod("emptyFormParam", String.class));
}
interface HeaderParams {
@POST void logout(@HeaderParam("Auth-Token") String token);
@GET Response emptyHeaderParam(@HeaderParam("") String empty);
}
@Test public void headerParamsParseIntoIndexToName() throws Exception {
@ -264,6 +341,11 @@ public class JAXRSContractTest { @@ -264,6 +341,11 @@ public class JAXRSContractTest {
assertEquals(md.indexToName().get(0), ImmutableSet.of("Auth-Token"));
}
@Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "HeaderParam.value\\(\\) was empty on parameter 0")
public void emptyHeaderParam() throws Exception {
contract.parseAndValidatateMetadata(HeaderParams.class.getDeclaredMethod("emptyHeaderParam", String.class));
}
interface WithObservable {
@GET @Path("/") Observable<List<String>> valid();

0
feign-jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java → jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java

0
feign-ribbon/README.md → ribbon/README.md

0
feign-ribbon/src/main/java/feign/ribbon/LBClient.java → ribbon/src/main/java/feign/ribbon/LBClient.java

0
feign-ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java → ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java

0
feign-ribbon/src/main/java/feign/ribbon/RibbonModule.java → ribbon/src/main/java/feign/ribbon/RibbonModule.java

0
feign-ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java → ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java

0
feign-ribbon/src/test/java/feign/ribbon/RibbonClientTest.java → ribbon/src/test/java/feign/ribbon/RibbonClientTest.java

6
settings.gradle

@ -1,2 +1,6 @@ @@ -1,2 +1,6 @@
rootProject.name='feign'
include 'feign-core', 'feign-gson', 'feign-jaxrs', 'feign-ribbon', 'examples:feign-example-cli'
include 'core', 'gson', 'jaxrs', 'ribbon', 'example-github', 'example-wikipedia'
rootProject.children.each { childProject ->
childProject.name = 'feign-' + childProject.name
}

Loading…
Cancel
Save