Compare commits

...

18 Commits
master ... 3.x

  1. 18
      CHANGES.md
  2. 22
      build.gradle
  3. 79
      examples/feign-example-cli/src/main/java/feign/example/cli/GitHubExample.java
  4. 6
      examples/feign-example-github/build.gradle
  5. 87
      examples/feign-example-github/src/main/java/feign/example/github/GitHubExample.java
  6. 49
      examples/feign-example-wikipedia/build.gradle
  7. 87
      examples/feign-example-wikipedia/src/main/java/feign/example/wikipedia/ResponseDecoder.java
  8. 145
      examples/feign-example-wikipedia/src/main/java/feign/example/wikipedia/WikipediaExample.java
  9. 75
      feign-core/src/main/java/feign/Logger.java
  10. 37
      feign-core/src/main/java/feign/MethodHandler.java
  11. 37
      feign-core/src/main/java/feign/RequestTemplate.java
  12. 132
      feign-core/src/test/java/feign/FeignTest.java
  13. 275
      feign-core/src/test/java/feign/LoggerTest.java
  14. 32
      feign-core/src/test/java/feign/RequestTemplateTest.java
  15. 52
      feign-gson/src/test/java/feign/gson/GsonModuleTest.java
  16. 37
      feign-jaxrs/README.md
  17. 53
      feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java
  18. 119
      feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java
  19. 2
      gradle.properties
  20. 2
      settings.gradle

18
CHANGES.md

@ -1,3 +1,21 @@ @@ -1,3 +1,21 @@
### 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>`.
* `Observer<T>` replaces `IncrementalCallback<T>` and is passed to `Observable.subscribe()`.
* On `Subscription.unsubscribe()`, `Observer.onNext()` will stop being called.
### Version 3.1
* Log when an http request is retried or a response fails due to an IOException.
### Version 3.0
* Added support for asynchronous callbacks via `IncrementalCallback<T>` and `IncrementalDecoder.TextStream<T>`.
* Wire is now Logger, with configurable Logger.Level.

22
build.gradle

@ -35,13 +35,13 @@ project(':feign-core') { @@ -35,13 +35,13 @@ project(':feign-core') {
}
dependencies {
compile 'com.squareup.dagger:dagger:1.0.1'
provided 'com.squareup.dagger:dagger-compiler:1.0.1'
compile 'com.squareup.dagger:dagger:1.1.0'
provided 'com.squareup.dagger:dagger-compiler:1.1.0'
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,12 +55,12 @@ project(':feign-jaxrs') { @@ -55,12 +55,12 @@ 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'
provided 'com.squareup.dagger:dagger-compiler:1.1.0'
// for example classes
testCompile project(':feign-core').sourceSets.test.output
testCompile 'com.google.guava:guava:14.0.1'
testCompile 'com.google.code.gson:gson:2.2.4'
testCompile 'org.testng:testng:6.8.1'
testCompile 'org.testng:testng:6.8.5'
}
}
@ -74,8 +74,8 @@ project(':feign-gson') { @@ -74,8 +74,8 @@ 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'
provided 'com.squareup.dagger:dagger-compiler:1.1.0'
testCompile 'org.testng:testng:6.8.5'
}
}
@ -89,8 +89,8 @@ project(':feign-ribbon') { @@ -89,8 +89,8 @@ 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'
provided 'com.squareup.dagger:dagger-compiler:1.1.0'
testCompile 'org.testng:testng:6.8.5'
testCompile 'com.google.mockwebserver:mockwebserver:20130706'
}
}

79
examples/feign-example-cli/src/main/java/feign/example/cli/GitHubExample.java

@ -1,79 +0,0 @@ @@ -1,79 +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.example.cli;
import com.google.gson.Gson;
import java.io.Reader;
import java.lang.reflect.Type;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Named;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
import feign.Feign;
import feign.RequestLine;
import feign.codec.Decoder;
/**
* adapted from {@code com.example.retrofit.GitHubClient}
*/
public class GitHubExample {
interface GitHub {
@RequestLine("GET /repos/{owner}/{repo}/contributors")
List<Contributor> contributors(@Named("owner") String owner, @Named("repo") String repo);
}
static class Contributor {
String login;
int contributions;
}
public static void main(String... args) {
GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule());
// Fetch and print a list of the contributors to this library.
List<Contributor> contributors = github.contributors("netflix", "feign");
for (Contributor contributor : contributors) {
System.out.println(contributor.login + " (" + contributor.contributions + ")");
}
}
/**
* Here's how to wire gson deserialization.
*/
@Module(overrides = true, library = true)
static class GsonModule {
@Provides @Singleton Map<String, Decoder> decoders() {
Map<String, Decoder> decoders = new LinkedHashMap<String, Decoder>();
decoders.put("GitHub", jsonDecoder);
return decoders;
}
final Decoder jsonDecoder = new Decoder() {
Gson gson = new Gson();
@Override public Object decode(String methodKey, Reader reader, Type type) {
return gson.fromJson(reader, type);
}
};
}
}

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

@ -1,8 +1,8 @@ @@ -1,8 +1,8 @@
apply plugin: 'java'
dependencies {
compile 'com.netflix.feign:feign-core:2.0.0'
compile 'com.google.code.gson:gson:2.2.4'
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'
}
@ -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

87
examples/feign-example-github/src/main/java/feign/example/github/GitHubExample.java

@ -0,0 +1,87 @@ @@ -0,0 +1,87 @@
/*
* 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.github;
import feign.Feign;
import feign.IncrementalCallback;
import feign.RequestLine;
import feign.gson.GsonModule;
import javax.inject.Named;
import java.util.List;
import java.util.concurrent.CountDownLatch;
/**
* adapted from {@code com.example.retrofit.GitHubClient}
*/
public class GitHubExample {
interface GitHub {
@RequestLine("GET /repos/{owner}/{repo}/contributors")
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);
}
static class Contributor {
String login;
int contributions;
}
public static void main(String... args) throws InterruptedException {
GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule());
System.out.println("Let's fetch and print a list of the contributors to this library.");
List<Contributor> contributors = github.contributors("netflix", "feign");
for (Contributor contributor : contributors) {
System.out.println(contributor.login + " (" + contributor.contributions + ")");
}
final CountDownLatch latch = new CountDownLatch(1);
System.out.println("Now, let's do it as an incremental async task.");
IncrementalCallback<Contributor> task = new IncrementalCallback<Contributor>() {
public int count;
// parsed directly from the text stream without an intermediate collection.
@Override public void onNext(Contributor contributor) {
System.out.println(contributor.login + " (" + contributor.contributions + ")");
count++;
}
@Override public void onSuccess() {
System.out.println("found " + count + " contributors");
latch.countDown();
}
@Override public void onFailure(Throwable cause) {
cause.printStackTrace();
latch.countDown();
}
};
// fire a task in the background.
github.contributors("netflix", "feign", task);
// wait for the task to complete.
latch.await();
System.exit(0);
}
}

49
examples/feign-example-wikipedia/build.gradle

@ -0,0 +1,49 @@ @@ -0,0 +1,49 @@
apply plugin: 'java'
dependencies {
compile 'com.netflix.feign:feign-core:3.1.0'
compile 'com.netflix.feign:feign-gson:3.1.0'
provided 'com.squareup.dagger:dagger-compiler:1.0.1'
}
// 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
examples/feign-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
examples/feign-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;
}
};
}
}
}

75
feign-core/src/main/java/feign/Logger.java

@ -17,7 +17,9 @@ package feign; @@ -17,7 +17,9 @@ package feign;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.util.logging.FileHandler;
import java.util.logging.LogRecord;
import java.util.logging.SimpleFormatter;
@ -59,8 +61,8 @@ public abstract class Logger { @@ -59,8 +61,8 @@ public abstract class Logger {
public static class ErrorLogger extends Logger {
final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(Logger.class.getName());
@Override protected void log(Target<?> target, String format, Object... args) {
System.err.printf(format + "%n", args);
@Override protected void log(String configKey, String format, Object... args) {
System.err.printf(methodTag(configKey) + format + "%n", args);
}
}
@ -70,22 +72,22 @@ public abstract class Logger { @@ -70,22 +72,22 @@ public abstract class Logger {
public static class JavaLogger extends Logger {
final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(Logger.class.getName());
@Override void logRequest(Target<?> target, Level logLevel, Request request) {
@Override void logRequest(String configKey, Level logLevel, Request request) {
if (logger.isLoggable(java.util.logging.Level.FINE)) {
super.logRequest(target, logLevel, request);
super.logRequest(configKey, logLevel, request);
}
}
@Override
Response logAndRebufferResponse(Target<?> target, Level logLevel, Response response, long elapsedTime) throws IOException {
Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime) throws IOException {
if (logger.isLoggable(java.util.logging.Level.FINE)) {
return super.logAndRebufferResponse(target, logLevel, response, elapsedTime);
return super.logAndRebufferResponse(configKey, logLevel, response, elapsedTime);
}
return response;
}
@Override protected void log(Target<?> target, String format, Object... args) {
logger.fine(String.format(format, args));
@Override protected void log(String configKey, String format, Object... args) {
logger.fine(String.format(methodTag(configKey) + format, args));
}
/**
@ -110,16 +112,16 @@ public abstract class Logger { @@ -110,16 +112,16 @@ public abstract class Logger {
}
public static class NoOpLogger extends Logger {
@Override void logRequest(Target<?> target, Level logLevel, Request request) {
@Override void logRequest(String configKey, Level logLevel, Request request) {
}
@Override
Response logAndRebufferResponse(Target<?> target, Level logLevel, Response response, long elapsedTime) throws IOException {
Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime) throws IOException {
return response;
}
@Override
protected void log(Target<?> target, String format, Object... args) {
protected void log(String configKey, String format, Object... args) {
}
}
@ -127,19 +129,19 @@ public abstract class Logger { @@ -127,19 +129,19 @@ public abstract class Logger {
* Override to log requests and responses using your own implementation.
* Messages will be http request and response text.
*
* @param target useful if using MDC (Mapped Diagnostic Context) loggers
* @param format {@link java.util.Formatter format string}
* @param args arguments applied to {@code format}
* @param configKey value of {@link Feign#configKey(java.lang.reflect.Method)}
* @param format {@link java.util.Formatter format string}
* @param args arguments applied to {@code format}
*/
protected abstract void log(Target<?> target, String format, Object... args);
protected abstract void log(String configKey, String format, Object... args);
void logRequest(Target<?> target, Level logLevel, Request request) {
log(target, "---> %s %s HTTP/1.1", request.method(), request.url());
void logRequest(String configKey, Level logLevel, Request request) {
log(configKey, "---> %s %s HTTP/1.1", request.method(), request.url());
if (logLevel.ordinal() >= Level.HEADERS.ordinal()) {
for (String field : request.headers().keySet()) {
for (String value : valuesOrEmpty(request.headers(), field)) {
log(target, "%s: %s", field, value);
log(configKey, "%s: %s", field, value);
}
}
@ -147,27 +149,31 @@ public abstract class Logger { @@ -147,27 +149,31 @@ public abstract class Logger {
if (request.body() != null) {
bytes = request.body().getBytes(UTF_8).length;
if (logLevel.ordinal() >= Level.FULL.ordinal()) {
log(target, ""); // CRLF
log(target, "%s", request.body());
log(configKey, ""); // CRLF
log(configKey, "%s", request.body());
}
}
log(target, "---> END HTTP (%s-byte body)", bytes);
log(configKey, "---> END HTTP (%s-byte body)", bytes);
}
}
Response logAndRebufferResponse(Target<?> target, Level logLevel, Response response, long elapsedTime) throws IOException {
log(target, "<--- HTTP/1.1 %s %s (%sms)", response.status(), response.reason(), elapsedTime);
void logRetry(String configKey, Level logLevel) {
log(configKey, "---> RETRYING");
}
Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime) throws IOException {
log(configKey, "<--- HTTP/1.1 %s %s (%sms)", response.status(), response.reason(), elapsedTime);
if (logLevel.ordinal() >= Level.HEADERS.ordinal()) {
for (String field : response.headers().keySet()) {
for (String value : valuesOrEmpty(response.headers(), field)) {
log(target, "%s: %s", field, value);
log(configKey, "%s: %s", field, value);
}
}
if (response.body() != null) {
if (logLevel.ordinal() >= Level.FULL.ordinal()) {
log(target, ""); // CRLF
log(configKey, ""); // CRLF
}
Reader body = response.body().asReader();
@ -178,11 +184,11 @@ public abstract class Logger { @@ -178,11 +184,11 @@ public abstract class Logger {
while ((line = reader.readLine()) != null) {
buffered.append(line);
if (logLevel.ordinal() >= Level.FULL.ordinal()) {
log(target, "%s", line);
log(configKey, "%s", line);
}
}
String bodyAsString = buffered.toString();
log(target, "<--- END HTTP (%s-byte body)", bodyAsString.getBytes(UTF_8).length);
log(configKey, "<--- END HTTP (%s-byte body)", bodyAsString.getBytes(UTF_8).length);
return Response.create(response.status(), response.reason(), response.headers(), bodyAsString);
} finally {
ensureClosed(response.body());
@ -191,4 +197,19 @@ public abstract class Logger { @@ -191,4 +197,19 @@ public abstract class Logger {
}
return response;
}
IOException logIOException(String configKey, Level logLevel, IOException ioe, long elapsedTime) {
log(configKey, "<--- ERROR %s: %s (%sms)", ioe.getClass().getSimpleName(), ioe.getMessage(), elapsedTime);
if (logLevel.ordinal() >= Level.FULL.ordinal()) {
StringWriter sw = new StringWriter();
ioe.printStackTrace(new PrintWriter(sw));
log(configKey, sw.toString());
log(configKey, "<--- END ERROR");
}
return ioe;
}
static String methodTag(String configKey) {
return new StringBuilder().append('[').append(configKey.substring(0, configKey.indexOf('('))).append("] ").toString();
}
}

37
feign-core/src/main/java/feign/MethodHandler.java

@ -56,10 +56,10 @@ abstract class MethodHandler { @@ -56,10 +56,10 @@ abstract class MethodHandler {
private final Lazy<Executor> httpExecutor;
private final Provider<Retryer> retryer;
private final Logger logger;
private final Logger.Level logLevel;
private final Provider<Logger.Level> logLevel;
@Inject Factory(Client client, @Named("http") Lazy<Executor> httpExecutor, Provider<Retryer> retryer, Logger logger,
Logger.Level logLevel) {
Provider<Logger.Level> logLevel) {
this.client = checkNotNull(client, "client");
this.httpExecutor = checkNotNull(httpExecutor, "httpExecutor");
this.retryer = checkNotNull(retryer, "retryer");
@ -86,7 +86,7 @@ abstract class MethodHandler { @@ -86,7 +86,7 @@ abstract class MethodHandler {
private final IncrementalDecoder.TextStream<?> incDecoder;
private IncrementalCallbackMethodHandler(Target<?> target, Client client, Provider<Retryer> retryer, Logger logger,
Logger.Level logLevel, MethodMetadata metadata,
Provider<Logger.Level> logLevel, MethodMetadata metadata,
BuildTemplateFromArgs buildTemplateFromArgs, Options options,
IncrementalDecoder.TextStream<?> incDecoder, ErrorDecoder errorDecoder,
Lazy<Executor> httpExecutor) {
@ -150,7 +150,7 @@ abstract class MethodHandler { @@ -150,7 +150,7 @@ abstract class MethodHandler {
private final Decoder.TextStream<?> decoder;
private SynchronousMethodHandler(Target<?> target, Client client, Provider<Retryer> retryer, Logger logger,
Logger.Level logLevel, MethodMetadata metadata,
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);
@ -178,15 +178,16 @@ abstract class MethodHandler { @@ -178,15 +178,16 @@ abstract class MethodHandler {
protected final Client client;
protected final Provider<Retryer> retryer;
protected final Logger logger;
protected final Logger.Level logLevel;
protected final Provider<Logger.Level> logLevel;
protected final BuildTemplateFromArgs buildTemplateFromArgs;
protected final Options options;
protected final ErrorDecoder errorDecoder;
private MethodHandler(Target<?> target, Client client, Provider<Retryer> retryer, Logger logger,
Logger.Level logLevel, MethodMetadata metadata, BuildTemplateFromArgs buildTemplateFromArgs,
Options options, ErrorDecoder errorDecoder) {
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);
@ -206,6 +207,9 @@ abstract class MethodHandler { @@ -206,6 +207,9 @@ abstract class MethodHandler {
return executeAndDecode(argv, template);
} catch (RetryableException e) {
retryer.continueOrPropagate(e);
if (logLevel.get() != Logger.Level.NONE) {
logger.logRetry(metadata.configKey(), logLevel.get());
}
continue;
}
}
@ -214,8 +218,8 @@ abstract class MethodHandler { @@ -214,8 +218,8 @@ abstract class MethodHandler {
public Object executeAndDecode(Object[] argv, RequestTemplate template) throws Throwable {
Request request = targetRequest(template);
if (logLevel.ordinal() > Logger.Level.NONE.ordinal()) {
logger.logRequest(target, logLevel, request);
if (logLevel.get() != Logger.Level.NONE) {
logger.logRequest(metadata.configKey(), logLevel.get(), request);
}
Response response;
@ -223,13 +227,17 @@ abstract class MethodHandler { @@ -223,13 +227,17 @@ abstract class MethodHandler {
try {
response = client.execute(request, options);
} catch (IOException e) {
if (logLevel.get() != Logger.Level.NONE) {
logger.logIOException(metadata.configKey(), logLevel.get(), e, elapsedTime(start));
}
throw errorExecuting(request, e);
}
long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
try {
if (logLevel.ordinal() > Logger.Level.NONE.ordinal()) {
response = logger.logAndRebufferResponse(target, logLevel, response, elapsedTime);
if (logLevel.get() != Logger.Level.NONE) {
response = logger.logAndRebufferResponse(metadata.configKey(), logLevel.get(), response, elapsedTime);
}
if (response.status() >= 200 && response.status() < 300) {
return decode(argv, response);
@ -237,6 +245,9 @@ abstract class MethodHandler { @@ -237,6 +245,9 @@ abstract class MethodHandler {
throw errorDecoder.decode(metadata.configKey(), response);
}
} catch (IOException e) {
if (logLevel.get() != Logger.Level.NONE) {
logger.logIOException(metadata.configKey(), logLevel.get(), e, elapsedTime);
}
throw errorReading(request, response, e);
} finally {
ensureClosed(response.body());
@ -247,5 +258,9 @@ abstract class MethodHandler { @@ -247,5 +258,9 @@ abstract class MethodHandler {
return target.apply(new RequestTemplate(template));
}
protected long elapsedTime(long start) {
return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
}
protected abstract Object decode(Object[] argv, Response response) throws Throwable;
}

37
feign-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;
@ -98,9 +99,7 @@ public final class RequestTemplate implements Serializable { @@ -98,9 +99,7 @@ public final class RequestTemplate implements Serializable {
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));
replaceQueryValues(encoded);
String resolvedUrl = expand(url.toString(), encoded).replace("%2F", "/");
url = new StringBuilder(resolvedUrl);
@ -505,6 +504,37 @@ public final class RequestTemplate implements Serializable { @@ -505,6 +504,37 @@ 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) {
values.add(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 +554,5 @@ public final class RequestTemplate implements Serializable { @@ -524,6 +554,5 @@ public final class RequestTemplate implements Serializable {
return queryBuilder.insert(0, '?').toString();
}
private static final long serialVersionUID = 1L;
}

132
feign-core/src/test/java/feign/FeignTest.java

@ -124,7 +124,7 @@ public class FeignTest { @@ -124,7 +124,7 @@ public class FeignTest {
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 AtomicBoolean success = new AtomicBoolean();
@ -158,7 +158,7 @@ public class FeignTest { @@ -158,7 +158,7 @@ public class FeignTest {
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 AtomicBoolean success = new AtomicBoolean();
@ -192,7 +192,7 @@ public class FeignTest { @@ -192,7 +192,7 @@ public class FeignTest {
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 AtomicBoolean success = new AtomicBoolean();
@ -226,7 +226,7 @@ public class FeignTest { @@ -226,7 +226,7 @@ public class FeignTest {
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()),
@ -243,7 +243,7 @@ public class FeignTest { @@ -243,7 +243,7 @@ public class FeignTest {
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()),
@ -260,7 +260,7 @@ public class FeignTest { @@ -260,7 +260,7 @@ public class FeignTest {
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]");
@ -275,29 +275,32 @@ public class FeignTest { @@ -275,29 +275,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 {
@ -312,7 +315,7 @@ public class FeignTest { @@ -312,7 +315,7 @@ public class FeignTest {
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();
@ -323,23 +326,26 @@ public class FeignTest { @@ -323,23 +326,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.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 {
@ -348,6 +354,21 @@ public class FeignTest { @@ -348,6 +354,21 @@ 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.
*/
@ -358,20 +379,8 @@ public class FeignTest { @@ -358,20 +379,8 @@ public class FeignTest {
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 {
@ -380,6 +389,18 @@ public class FeignTest { @@ -380,6 +389,18 @@ 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();
@ -387,17 +408,8 @@ public class FeignTest { @@ -387,17 +408,8 @@ public class FeignTest {
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 {
@ -420,7 +432,7 @@ public class FeignTest { @@ -420,7 +432,7 @@ public class FeignTest {
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 {
@ -436,7 +448,7 @@ public class FeignTest { @@ -436,7 +448,7 @@ public class FeignTest {
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);

275
feign-core/src/test/java/feign/LoggerTest.java

@ -15,13 +15,11 @@ @@ -15,13 +15,11 @@
*/
package feign;
import com.google.common.collect.ImmutableMap;
import com.google.common.base.Joiner;
import com.google.mockwebserver.MockResponse;
import com.google.mockwebserver.MockWebServer;
import dagger.Provides;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.codec.StringDecoder;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
@ -32,18 +30,19 @@ import java.io.IOException; @@ -32,18 +30,19 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import static dagger.Provides.Type.SET;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;
@Test
public class LoggerTest {
Logger logger = new Logger() {
@Override protected void log(Target<?> target, String format, Object... args) {
messages.add(String.format(format, args));
@Override protected void log(String configKey, String format, Object... args) {
messages.add(methodTag(configKey) + String.format(format, args));
}
};
@ -64,38 +63,38 @@ public class LoggerTest { @@ -64,38 +63,38 @@ public class LoggerTest {
}
@DataProvider(name = "levelToOutput")
public Object[][] createData() {
public Object[][] levelToOutput() {
Object[][] data = new Object[4][2];
data[0][0] = Logger.Level.NONE;
data[0][1] = Arrays.<String>asList();
data[1][0] = Logger.Level.BASIC;
data[1][1] = Arrays.asList(
"---> POST http://localhost:[0-9]+/ HTTP/1.1",
"<--- HTTP/1.1 200 OK \\([0-9]+ms\\)"
"\\[SendsStuff#login\\] ---> POST http://localhost:[0-9]+/ HTTP/1.1",
"\\[SendsStuff#login\\] <--- HTTP/1.1 200 OK \\([0-9]+ms\\)"
);
data[2][0] = Logger.Level.HEADERS;
data[2][1] = Arrays.asList(
"---> POST http://localhost:[0-9]+/ HTTP/1.1",
"Content-Type: application/json",
"Content-Length: 80",
"---> END HTTP \\(80-byte body\\)",
"<--- HTTP/1.1 200 OK \\([0-9]+ms\\)",
"Content-Length: 3",
"<--- END HTTP \\(3-byte body\\)"
"\\[SendsStuff#login\\] ---> POST http://localhost:[0-9]+/ HTTP/1.1",
"\\[SendsStuff#login\\] Content-Type: application/json",
"\\[SendsStuff#login\\] Content-Length: 80",
"\\[SendsStuff#login\\] ---> END HTTP \\(80-byte body\\)",
"\\[SendsStuff#login\\] <--- HTTP/1.1 200 OK \\([0-9]+ms\\)",
"\\[SendsStuff#login\\] Content-Length: 3",
"\\[SendsStuff#login\\] <--- END HTTP \\(3-byte body\\)"
);
data[3][0] = Logger.Level.FULL;
data[3][1] = Arrays.asList(
"---> POST http://localhost:[0-9]+/ HTTP/1.1",
"Content-Type: application/json",
"Content-Length: 80",
"",
"\\{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"\\}",
"---> END HTTP \\(80-byte body\\)",
"<--- HTTP/1.1 200 OK \\([0-9]+ms\\)",
"Content-Length: 3",
"",
"foo",
"<--- END HTTP \\(3-byte body\\)"
"\\[SendsStuff#login\\] ---> POST http://localhost:[0-9]+/ HTTP/1.1",
"\\[SendsStuff#login\\] Content-Type: application/json",
"\\[SendsStuff#login\\] Content-Length: 80",
"\\[SendsStuff#login\\] ",
"\\[SendsStuff#login\\] \\{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"\\}",
"\\[SendsStuff#login\\] ---> END HTTP \\(80-byte body\\)",
"\\[SendsStuff#login\\] <--- HTTP/1.1 200 OK \\([0-9]+ms\\)",
"\\[SendsStuff#login\\] Content-Length: 3",
"\\[SendsStuff#login\\] ",
"\\[SendsStuff#login\\] foo",
"\\[SendsStuff#login\\] <--- END HTTP \\(3-byte body\\)"
);
return data;
}
@ -103,36 +102,116 @@ public class LoggerTest { @@ -103,36 +102,116 @@ public class LoggerTest {
@Test(dataProvider = "levelToOutput")
public void levelEmits(final Logger.Level logLevel, List<String> expectedMessages) throws IOException, InterruptedException {
final MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setResponseCode(200).setBody("foo"));
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();
}
};
}
try {
SendsStuff api = Feign.create(SendsStuff.class, "http://localhost:" + server.getUrl("").getPort(),
new DefaultModule(logger, logLevel));
@Provides @Singleton Logger logger() {
return logger;
}
api.login("netflix", "denominator", "password");
@Provides @Singleton Logger.Level level() {
return logLevel;
assertEquals(messages.size(), expectedMessages.size());
for (int i = 0; i < messages.size(); i++) {
assertTrue(messages.get(i).matches(expectedMessages.get(i)), messages.get(i));
}
assertEquals(new String(server.takeRequest().getBody()),
"{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}");
} finally {
server.shutdown();
}
}
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];
data[0][0] = Logger.Level.NONE;
data[0][1] = Arrays.<String>asList();
data[1][0] = Logger.Level.BASIC;
data[1][1] = Arrays.asList(
"\\[SendsStuff#login\\] ---> POST http://localhost:[0-9]+/ HTTP/1.1",
"\\[SendsStuff#login\\] <--- HTTP/1.1 200 OK \\([0-9]+ms\\)",
"\\[SendsStuff#login\\] <--- ERROR SocketTimeoutException: Read timed out \\([0-9]+ms\\)"
);
data[2][0] = Logger.Level.HEADERS;
data[2][1] = Arrays.asList(
"\\[SendsStuff#login\\] ---> POST http://localhost:[0-9]+/ HTTP/1.1",
"\\[SendsStuff#login\\] Content-Type: application/json",
"\\[SendsStuff#login\\] Content-Length: 80",
"\\[SendsStuff#login\\] ---> END HTTP \\(80-byte body\\)",
"\\[SendsStuff#login\\] <--- HTTP/1.1 200 OK \\([0-9]+ms\\)",
"\\[SendsStuff#login\\] Content-Length: 3",
"\\[SendsStuff#login\\] <--- ERROR SocketTimeoutException: Read timed out \\([0-9]+ms\\)"
);
data[3][0] = Logger.Level.FULL;
data[3][1] = Arrays.asList(
"\\[SendsStuff#login\\] ---> POST http://localhost:[0-9]+/ HTTP/1.1",
"\\[SendsStuff#login\\] Content-Type: application/json",
"\\[SendsStuff#login\\] Content-Length: 80",
"\\[SendsStuff#login\\] ",
"\\[SendsStuff#login\\] \\{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"\\}",
"\\[SendsStuff#login\\] ---> END HTTP \\(80-byte body\\)",
"\\[SendsStuff#login\\] <--- HTTP/1.1 200 OK \\([0-9]+ms\\)",
"\\[SendsStuff#login\\] Content-Length: 3",
"\\[SendsStuff#login\\] ",
"\\[SendsStuff#login\\] <--- ERROR SocketTimeoutException: Read timed out \\([0-9]+ms\\)",
"\\[SendsStuff#login\\] java.net.SocketTimeoutException: Read timed out.*",
"\\[SendsStuff#login\\] <--- END ERROR"
);
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();
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");
assertEquals(messages.size(), expectedMessages.size());
for (int i = 0; i < messages.size(); i++) {
assertTrue(messages.get(i).matches(expectedMessages.get(i)), messages.get(i));
}
fail();
} catch (FeignException e) {
assertMessagesMatch(expectedMessages);
assertEquals(new String(server.takeRequest().getBody()),
"{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}");
@ -140,4 +219,108 @@ public class LoggerTest { @@ -140,4 +219,108 @@ public class LoggerTest {
server.shutdown();
}
}
@DataProvider(name = "levelToUnknownHostOutput")
public Object[][] levelToUnknownHostOutput() {
Object[][] data = new Object[4][2];
data[0][0] = Logger.Level.NONE;
data[0][1] = Arrays.<String>asList();
data[1][0] = Logger.Level.BASIC;
data[1][1] = Arrays.asList(
"\\[SendsStuff#login\\] ---> POST http://robofu.abc/ HTTP/1.1",
"\\[SendsStuff#login\\] <--- ERROR UnknownHostException: robofu.abc \\([0-9]+ms\\)"
);
data[2][0] = Logger.Level.HEADERS;
data[2][1] = Arrays.asList(
"\\[SendsStuff#login\\] ---> POST http://robofu.abc/ HTTP/1.1",
"\\[SendsStuff#login\\] Content-Type: application/json",
"\\[SendsStuff#login\\] Content-Length: 80",
"\\[SendsStuff#login\\] ---> END HTTP \\(80-byte body\\)",
"\\[SendsStuff#login\\] <--- ERROR UnknownHostException: robofu.abc \\([0-9]+ms\\)"
);
data[3][0] = Logger.Level.FULL;
data[3][1] = Arrays.asList(
"\\[SendsStuff#login\\] ---> POST http://robofu.abc/ HTTP/1.1",
"\\[SendsStuff#login\\] Content-Type: application/json",
"\\[SendsStuff#login\\] Content-Length: 80",
"\\[SendsStuff#login\\] ",
"\\[SendsStuff#login\\] \\{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"\\}",
"\\[SendsStuff#login\\] ---> END HTTP \\(80-byte body\\)",
"\\[SendsStuff#login\\] <--- ERROR UnknownHostException: robofu.abc \\([0-9]+ms\\)",
"\\[SendsStuff#login\\] java.net.UnknownHostException: robofu.abc.*",
"\\[SendsStuff#login\\] <--- END ERROR"
);
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 {
try {
SendsStuff api = Feign.create(SendsStuff.class, "http://robofu.abc",
new DontRetryModule(), new DefaultModule(logger, logLevel));
api.login("netflix", "denominator", "password");
fail();
} catch (FeignException e) {
assertMessagesMatch(expectedMessages);
}
}
@dagger.Module(overrides = true, library = true)
static class RetryOnceModule {
@Provides Retryer retryer() {
return new Retryer() {
boolean retried;
@Override public void continueOrPropagate(RetryableException e) {
if (!retried) {
retried = true;
return;
}
throw e;
}
};
}
}
public void retryEmits() throws IOException, InterruptedException {
try {
SendsStuff api = Feign.create(SendsStuff.class, "http://robofu.abc",
new RetryOnceModule(), new DefaultModule(logger, Logger.Level.BASIC));
api.login("netflix", "denominator", "password");
fail();
} catch (FeignException e) {
assertMessagesMatch(Arrays.asList(
"\\[SendsStuff#login\\] ---> POST http://robofu.abc/ HTTP/1.1",
"\\[SendsStuff#login\\] <--- ERROR UnknownHostException: robofu.abc \\([0-9]+ms\\)",
"\\[SendsStuff#login\\] ---> RETRYING",
"\\[SendsStuff#login\\] ---> POST http://robofu.abc/ HTTP/1.1",
"\\[SendsStuff#login\\] <--- ERROR UnknownHostException: robofu.abc \\([0-9]+ms\\)"
));
}
}
private void assertMessagesMatch(List<String> expectedMessages) {
assertEquals(messages.size(), expectedMessages.size());
for (int i = 0; i < messages.size(); i++) {
assertTrue(Pattern.compile(expectedMessages.get(i), Pattern.DOTALL).matcher(messages.get(i)).matches(),
"Didn't match at message " + (i + 1) + ":\n" + Joiner.on('\n').join(messages));
}
}
}

32
feign-core/src/test/java/feign/RequestTemplateTest.java

@ -18,7 +18,6 @@ package feign; @@ -18,7 +18,6 @@ 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 static feign.RequestTemplate.expand;
@ -133,4 +132,35 @@ public class RequestTemplateTest { @@ -133,4 +132,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");
}
}

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

@ -39,15 +39,15 @@ import static org.testng.Assert.fail; @@ -39,15 +39,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);
@ -58,12 +58,13 @@ public class GsonModuleTest { @@ -58,12 +58,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>();
@ -76,11 +77,8 @@ public class GsonModuleTest { @@ -76,11 +77,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>();
@ -115,12 +113,13 @@ public class GsonModuleTest { @@ -115,12 +113,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>();
@ -132,12 +131,13 @@ public class GsonModuleTest { @@ -132,12 +131,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
feign-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

@ -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 {
@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();
}
}

119
feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java

@ -24,6 +24,7 @@ import feign.MethodMetadata; @@ -24,6 +24,7 @@ import feign.MethodMetadata;
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;
@ -43,14 +44,15 @@ import java.lang.reflect.Type; @@ -43,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;
@ -153,21 +155,48 @@ public class JAXRSContractTest { @@ -153,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 {
@ -192,6 +221,42 @@ public class JAXRSContractTest { @@ -192,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);
}
@ -214,6 +279,8 @@ public class JAXRSContractTest { @@ -214,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 {
@ -231,29 +298,40 @@ public class JAXRSContractTest { @@ -231,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 {
@ -263,6 +341,11 @@ public class JAXRSContractTest { @@ -263,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 WithIncrementalCallback {
@GET @Path("/") void valid(IncrementalCallback<List<String>> one);

2
gradle.properties

@ -1 +1 @@ @@ -1 +1 @@
version=3.0.0-SNAPSHOT
version=3.3.1-SNAPSHOT

2
settings.gradle

@ -1,2 +1,2 @@ @@ -1,2 +1,2 @@
rootProject.name='feign'
include 'feign-core', 'feign-gson', 'feign-jaxrs', 'feign-ribbon', 'examples:feign-example-cli'
include 'feign-core', 'feign-gson', 'feign-jaxrs', 'feign-ribbon', 'examples:feign-example-github', 'examples:feign-example-wikipedia'

Loading…
Cancel
Save