From 697fd66aae9beed107e13f49a741455f1d9d8dd9 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Sun, 18 Mar 2012 01:07:52 -0700 Subject: [PATCH 001/179] Initial commit, working with Maven Central --- .classpath | 9 + .gitignore | 40 ++++ .project | 39 ++++ ....springsource.sts.gradle.core.import.prefs | 9 + .../com.springsource.sts.gradle.core.prefs | 4 + .../com.springsource.sts.gradle.refresh.prefs | 9 + build.gradle | 48 +++++ codequality/checkstyle.xml | 188 ++++++++++++++++++ gradle/check.gradle | 16 ++ gradle/convention.gradle | 45 +++++ gradle/maven.gradle | 59 ++++++ gradle/netflix-oss.gradle | 1 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 39752 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 164 +++++++++++++++ gradlew.bat | 90 +++++++++ settings.gradle | 1 + template-client/.classpath | 11 + template-client/.project | 19 ++ .../com.springsource.sts.gradle.core.prefs | 4 + .../com.springsource.sts.gradle.refresh.prefs | 9 + .../netflix/template/client/TalkClient.class | Bin 0 -> 2256 bytes .../template/common/Conversation.class | Bin 0 -> 214 bytes .../netflix/template/common/Sentence.class | Bin 0 -> 784 bytes .../netflix/template/client/TalkClient.java | 36 ++++ .../netflix/template/common/Conversation.java | 6 + .../com/netflix/template/common/Sentence.java | 25 +++ template-server/.classpath | 12 ++ template-server/.project | 19 ++ .../com.springsource.sts.gradle.core.prefs | 4 + .../com.springsource.sts.gradle.refresh.prefs | 9 + .../netflix/template/server/TalkServer.class | Bin 0 -> 872 bytes .../netflix/template/server/TalkServer.java | 26 +++ .../src/main/webapp/WEB-INF/web.xml | 25 +++ 34 files changed, 933 insertions(+) create mode 100644 .classpath create mode 100644 .gitignore create mode 100644 .project create mode 100644 .settings/gradle/com.springsource.sts.gradle.core.import.prefs create mode 100644 .settings/gradle/com.springsource.sts.gradle.core.prefs create mode 100644 .settings/gradle/com.springsource.sts.gradle.refresh.prefs create mode 100644 build.gradle create mode 100644 codequality/checkstyle.xml create mode 100644 gradle/check.gradle create mode 100644 gradle/convention.gradle create mode 100644 gradle/maven.gradle create mode 100644 gradle/netflix-oss.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 template-client/.classpath create mode 100644 template-client/.project create mode 100644 template-client/.settings/gradle/com.springsource.sts.gradle.core.prefs create mode 100644 template-client/.settings/gradle/com.springsource.sts.gradle.refresh.prefs create mode 100644 template-client/bin/com/netflix/template/client/TalkClient.class create mode 100644 template-client/bin/com/netflix/template/common/Conversation.class create mode 100644 template-client/bin/com/netflix/template/common/Sentence.class create mode 100644 template-client/src/main/java/com/netflix/template/client/TalkClient.java create mode 100644 template-client/src/main/java/com/netflix/template/common/Conversation.java create mode 100644 template-client/src/main/java/com/netflix/template/common/Sentence.java create mode 100644 template-server/.classpath create mode 100644 template-server/.project create mode 100644 template-server/.settings/gradle/com.springsource.sts.gradle.core.prefs create mode 100644 template-server/.settings/gradle/com.springsource.sts.gradle.refresh.prefs create mode 100644 template-server/bin/com/netflix/template/server/TalkServer.class create mode 100644 template-server/src/main/java/com/netflix/template/server/TalkServer.java create mode 100644 template-server/src/main/webapp/WEB-INF/web.xml diff --git a/.classpath b/.classpath new file mode 100644 index 0000000000..b1ae8bae1c --- /dev/null +++ b/.classpath @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..618e741f86 --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +# Compiled source # +################### +*.com +*.class +*.dll +*.exe +*.o +*.so + +# Packages # +############ +# it's better to unpack these files and commit the raw source +# git has its own built in compression methods +*.7z +*.dmg +*.gz +*.iso +*.jar +*.rar +*.tar +*.zip + +# Logs and databases # +###################### +*.log + +# OS generated files # +###################### +.DS_Store* +ehthumbs.db +Icon? +Thumbs.db + +# Editor Files # +################ +*~ + +# Gradle Files # +################ +.gradle diff --git a/.project b/.project new file mode 100644 index 0000000000..f2d845e45a --- /dev/null +++ b/.project @@ -0,0 +1,39 @@ + + + gradle-template + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + com.springsource.sts.gradle.core.nature + org.eclipse.jdt.core.javanature + org.eclipse.jdt.groovy.core.groovyNature + + + + 1332049227118 + + 10 + + org.eclipse.ui.ide.orFilterMatcher + + + org.eclipse.ui.ide.multiFilter + 1.0-projectRelativePath-equals-true-false-template-server + + + org.eclipse.ui.ide.multiFilter + 1.0-projectRelativePath-equals-true-false-template-client + + + + + + diff --git a/.settings/gradle/com.springsource.sts.gradle.core.import.prefs b/.settings/gradle/com.springsource.sts.gradle.core.import.prefs new file mode 100644 index 0000000000..e86c91081f --- /dev/null +++ b/.settings/gradle/com.springsource.sts.gradle.core.import.prefs @@ -0,0 +1,9 @@ +#com.springsource.sts.gradle.core.preferences.GradleImportPreferences +#Sat Mar 17 22:40:13 PDT 2012 +enableAfterTasks=true +afterTasks=afterEclipseImport; +enableDependendencyManagement=true +enableBeforeTasks=true +projects=;template-client;template-server; +enableDSLD=true +beforeTasks=cleanEclipse;eclipse; diff --git a/.settings/gradle/com.springsource.sts.gradle.core.prefs b/.settings/gradle/com.springsource.sts.gradle.core.prefs new file mode 100644 index 0000000000..445ff6da6f --- /dev/null +++ b/.settings/gradle/com.springsource.sts.gradle.core.prefs @@ -0,0 +1,4 @@ +#com.springsource.sts.gradle.core.preferences.GradleProjectPreferences +#Sat Mar 17 22:40:29 PDT 2012 +com.springsource.sts.gradle.rootprojectloc= +com.springsource.sts.gradle.linkedresources= diff --git a/.settings/gradle/com.springsource.sts.gradle.refresh.prefs b/.settings/gradle/com.springsource.sts.gradle.refresh.prefs new file mode 100644 index 0000000000..01e59693e7 --- /dev/null +++ b/.settings/gradle/com.springsource.sts.gradle.refresh.prefs @@ -0,0 +1,9 @@ +#com.springsource.sts.gradle.core.actions.GradleRefreshPreferences +#Sat Mar 17 22:40:27 PDT 2012 +enableAfterTasks=true +afterTasks=afterEclipseImport; +useHierarchicalNames=false +enableBeforeTasks=true +addResourceFilters=true +enableDSLD=true +beforeTasks=cleanEclipse;eclipse; diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..5297034a51 --- /dev/null +++ b/build.gradle @@ -0,0 +1,48 @@ +// Establish version and status +ext.releaseVersion = '1.1.3' // TEMPLATE: Set to latest release +ext.githubProjectName = rootProject.name // TEMPLATE: change to match github project, if it doesn't match project name + +apply from: file('gradle/convention.gradle') +apply from: file('gradle/maven.gradle') +apply from: file('gradle/check.gradle') + +subprojects +{ + group = 'com.netflix' + + repositories + { + mavenCentral() + } + + dependencies + { + compile 'javax.ws.rs:jsr311-api:1.1.1' + compile 'com.sun.jersey:jersey-core:1.11' + testCompile 'org.testng:testng:6.1.1' + testCompile 'org.mockito:mockito-core:1.8.5' + } +} + +project(':template-client') +{ + dependencies + { + compile 'org.slf4j:slf4j-api:1.6.3' + compile 'com.sun.jersey:jersey-client:1.11' + } +} + +project(':template-server') +{ + apply plugin: 'war' + apply plugin: 'jetty' + dependencies + { + compile 'com.sun.jersey:jersey-server:1.11' + compile 'com.sun.jersey:jersey-servlet:1.11' + compile project(':template-client') + testCompile 'org.mockito:mockito-core:1.8.5' + } +} + diff --git a/codequality/checkstyle.xml b/codequality/checkstyle.xml new file mode 100644 index 0000000000..3c8a8e6c75 --- /dev/null +++ b/codequality/checkstyle.xml @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle/check.gradle b/gradle/check.gradle new file mode 100644 index 0000000000..cf6f0461ae --- /dev/null +++ b/gradle/check.gradle @@ -0,0 +1,16 @@ +subprojects { + // Checkstyle + apply plugin: 'checkstyle' + tasks.withType(Checkstyle) { ignoreFailures = true } + checkstyle { + ignoreFailures = true // Waiting on GRADLE-2163 + configFile = rootProject.file('codequality/checkstyle.xml') + } + + // FindBugs + apply plugin: 'findbugs' + + // PMD + apply plugin: 'pmd' + +} diff --git a/gradle/convention.gradle b/gradle/convention.gradle new file mode 100644 index 0000000000..a3fc06dd04 --- /dev/null +++ b/gradle/convention.gradle @@ -0,0 +1,45 @@ + +ext.performingRelease = project.hasProperty('release') && Boolean.parseBoolean(project.release) +def versionPostfix = performingRelease?'':'-SNAPSHOT' +version = "${releaseVersion}${versionPostfix}" +status = performingRelease?'release':'snapshot' + +subprojects +{ + apply plugin: 'java' // Plugin as major conventions + + version = rootProject.version + + sourceCompatibility = 1.6 + + // GRADLE-2087 workaround, perform after java plugin + status = rootProject.status + + task sourcesJar(type: Jar, dependsOn:classes) { + classifier = 'sources' + from sourceSets.main.allSource + } + + task javadocJar(type: Jar, dependsOn:javadoc) { + classifier = 'javadoc' + from javadoc.destinationDir + } + + artifacts { + archives jar + archives sourcesJar + archives javadocJar + } +} + +task aggregateJavadoc(type: Javadoc) { + description = 'Aggregate all subproject docs into a single docs directory' + source subprojects.collect {project -> project.sourceSets.main.allJava } + classpath = files(subprojects.collect {project -> project.sourceSets.main.compileClasspath}) + destinationDir = new File(projectDir, 'doc') +} + +// Generate wrapper, which is distributed as part of source to alleviate the need of installing gradle +task createWrapper(type: Wrapper) { + gradleVersion = '1.0-milestone-9' +} diff --git a/gradle/maven.gradle b/gradle/maven.gradle new file mode 100644 index 0000000000..8639564ce4 --- /dev/null +++ b/gradle/maven.gradle @@ -0,0 +1,59 @@ +// Maven side of things +subprojects { + apply plugin: 'maven' // Java plugin has to have been already applied for the conf2scope mappings to work + apply plugin: 'signing' + + signing { + required rootProject.performingRelease + sign configurations.archives + } + + /** + * Publishing to Maven Central example provided from http://jedicoder.blogspot.com/2011/11/automated-gradle-project-deployment-to.html + */ + task uploadMavenCentral(type:Upload) { + configuration = configurations.archives + dependsOn signArchives + doFirst { + repositories.mavenDeployer { + beforeDeployment { org.gradle.api.artifacts.maven.MavenDeployment deployment -> signing.signPom(deployment) } + + // To test deployment locally, use the following instead of oss.sonatype.org + //repository(url: "file://localhost/${rootProject.rootDir}/repo") + + repository(url: 'http://oss.sonatype.org/services/local/staging/deply/maven2/') { + authentication(userName: rootProject.sonatypeUsername, password: rootProject.sonatypePassword) + } + + // Prevent datastamp from being appending to artifacts during deployment + uniqueVersion = false + + // Closure to configure all the POM with extra info, common to all projects + pom.project { + parent { + groupId 'org.sonatype.oss' + artifactId 'oss-parent' + version '7' + } + url "https://github.com/Netflix/${rootProject.ext.githubProjectName}" + licenses { + license { + name 'The Apache Software License, Version 2.0' + url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + distribution 'repo' + } + } + scm { + connection "scm:git:git@github.com:Netflix/${rootProject.ext.githubProjectName}.git" + url "scm:git:git@github.com:Netflix/${rootProject.ext.githubProjectName}.git" + developerConnection "scm:git:git@github.com:Netflix/${rootProject.ext.githubProjectName}.git" + } + issueManagement { + system 'github' + url 'https://github.com/Netflix/${rootProject.ext.githubProjectName}/issues' + } + } + } + } + } +} diff --git a/gradle/netflix-oss.gradle b/gradle/netflix-oss.gradle new file mode 100644 index 0000000000..a87bc54efe --- /dev/null +++ b/gradle/netflix-oss.gradle @@ -0,0 +1 @@ +apply from: 'http://artifacts.netflix.com/gradle-netflix-local/artifactory.gradle' diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..2cb758a1c693a7cc7a489e4450ca1c700ac83049 GIT binary patch literal 39752 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" +APP_HOME="`pwd -P`" +cd "$SAVED" + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query businessSystem maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + JAVA_OPTS="$JAVA_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000000..8a0b282aa6 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000000..350f2f1b49 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include 'template-client','template-server' diff --git a/template-client/.classpath b/template-client/.classpath new file mode 100644 index 0000000000..cd1d475d6b --- /dev/null +++ b/template-client/.classpath @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/template-client/.project b/template-client/.project new file mode 100644 index 0000000000..66279e9c19 --- /dev/null +++ b/template-client/.project @@ -0,0 +1,19 @@ + + + template-client + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + com.springsource.sts.gradle.core.nature + org.eclipse.jdt.core.javanature + org.eclipse.jdt.groovy.core.groovyNature + + diff --git a/template-client/.settings/gradle/com.springsource.sts.gradle.core.prefs b/template-client/.settings/gradle/com.springsource.sts.gradle.core.prefs new file mode 100644 index 0000000000..1e7f022837 --- /dev/null +++ b/template-client/.settings/gradle/com.springsource.sts.gradle.core.prefs @@ -0,0 +1,4 @@ +#com.springsource.sts.gradle.core.preferences.GradleProjectPreferences +#Sat Mar 17 22:40:30 PDT 2012 +com.springsource.sts.gradle.rootprojectloc=.. +com.springsource.sts.gradle.linkedresources= diff --git a/template-client/.settings/gradle/com.springsource.sts.gradle.refresh.prefs b/template-client/.settings/gradle/com.springsource.sts.gradle.refresh.prefs new file mode 100644 index 0000000000..394fb107f1 --- /dev/null +++ b/template-client/.settings/gradle/com.springsource.sts.gradle.refresh.prefs @@ -0,0 +1,9 @@ +#com.springsource.sts.gradle.core.actions.GradleRefreshPreferences +#Sat Mar 17 22:40:30 PDT 2012 +enableAfterTasks=true +afterTasks=afterEclipseImport; +useHierarchicalNames=false +enableBeforeTasks=true +addResourceFilters=true +enableDSLD=true +beforeTasks=cleanEclipse;eclipse; diff --git a/template-client/bin/com/netflix/template/client/TalkClient.class b/template-client/bin/com/netflix/template/client/TalkClient.class new file mode 100644 index 0000000000000000000000000000000000000000..90bbaeb3532635ac2638cc61156bb545c24ba039 GIT binary patch literal 2256 zcma)7YgZdp6y2AkOfnq>LJ38x0_72)Fce=%uxhETwQ0cy!B}5IatT8xGjTGZ?Kgjt zx-@jzrOVI$D3|-r3?)3vhndW|x%cd|&)MhPfB*aQZvgYy)zBd@UiNCHD}yb^erg7? zR(GsGnq|k9ZeXri&g13qQ$tdqd&k~P&T}#UzP$B1$DB~bj=-Zk-`9=uBQi zR$%sY*ITv|NZ%}Y)hgq>9a?Ez#v2+24kfertijA17{nDu?ll7vjd zF@Z@9xx-Y#ni!bEw1Bp2IZe5;MU}PJEz(YY@^~qAjABOd(^D`7E|>uYs1~mq6zn@J zkX5fsw<0jp?r>iS#~j9yGH;`J&%pcmKp+)((SCtTtnyKE+}n*04J?AXOZhLwnm-Zn z^y0S*I1Pc13}{FRbQNq@K4{i9rN3rvI@DUG;FT?B-STZ^$BW5e+itM8!jiqZJyom@ z-9&AcInGLHJ8WcukuzivE1TY`zLWuF%NXKbI37jR;zdph>6T?ag}da(3ORNZiTSkV z=(vab8VUkq=Sr4=2Ut;sE|+DUjc_aX=s26RnzJqOqx<#O@Fja%;lGN6jv~I&uttTQ zE8Kku1MApe^HwEe9&6XivST$Ghr}N1UqGkNN!n?Ez6frq{ECB@vVXH6kcaV zI#La+WX-ZUV6!J?Ydr-^U}`%E>WikSbmDA#jbL$MZei}_%%5pUpQGBOH|(LqbI5=LofDw0rkCR?U#@{~aEGBWG!TUAf?aSAP4xT^k6ugf literal 0 HcmV?d00001 diff --git a/template-client/bin/com/netflix/template/common/Conversation.class b/template-client/bin/com/netflix/template/common/Conversation.class new file mode 100644 index 0000000000000000000000000000000000000000..86d42f57590c9fbee4a60450fd6eca4121b4cf6a GIT binary patch literal 214 zcmaKmF%H5o6hoZ?ZRrHJC`%);G9ob{G4uo>`mIu>KPZI4*%&wghe9M96O$!dw%_~n zd;!>^Dv$}(+KrMabk;m%pz&f=AQ{ckvD`bJ$X``3jtk5MR)d<9w2FIqIuE3SK-qhu zV7QN4_2&3*t|bn{ns%|(DNlE@R-kI#&1*UsO9JcP%O<_$0s^y03}lgDfgFjXNE(we H`B;7deJVPB literal 0 HcmV?d00001 diff --git a/template-client/bin/com/netflix/template/common/Sentence.class b/template-client/bin/com/netflix/template/common/Sentence.class new file mode 100644 index 0000000000000000000000000000000000000000..0083f334776617dd596f960095e6ab566be6b905 GIT binary patch literal 784 zcma)(TT2^36vzLQY%a!Jyhd%G3Z*0|bih6;LMinrR4@{KovhP1VRuH7iRNQjPy`?P z0s5iDb9Q4QUci?**ZrU0Is5<(Z4Lz)E{w>-eFu{T+e)uCd1N31l11u0Zh9o$3;@ zSS+J}qCl-}to}WYdwO`JdZ~;HRn%2O!|^m3_%kyS_|kq4D2-ijyo70X7eJI{=}D1)vPK{;^+21c=PK1?bcIx^)LToG z>Qm)ZiDxhtL#$$rYU`xQah)vd%cRDD*I2%yL<-0)pif?d+nB-aQ8&ZopMj<8ZP4h= VCs6t6dJK?4WvI>*w`N!$fCt$xmcRf2 literal 0 HcmV?d00001 diff --git a/template-client/src/main/java/com/netflix/template/client/TalkClient.java b/template-client/src/main/java/com/netflix/template/client/TalkClient.java new file mode 100644 index 0000000000..c3ebf86090 --- /dev/null +++ b/template-client/src/main/java/com/netflix/template/client/TalkClient.java @@ -0,0 +1,36 @@ +package com.netflix.template.client; + +import com.netflix.template.common.Conversation; +import com.netflix.template.common.Sentence; +import com.sun.jersey.api.client.Client; +import com.sun.jersey.api.client.WebResource; +import com.sun.jersey.api.client.filter.LoggingFilter; + +import javax.ws.rs.core.MediaType; + +public class TalkClient implements Conversation { + + WebResource webResource; + + TalkClient(String location) { + Client client = Client.create(); + client.addFilter(new LoggingFilter(System.out)); + webResource = client.resource(location + "/talk"); + } + + public Sentence greeting() { + Sentence s = webResource.accept(MediaType.APPLICATION_XML).get(Sentence.class); + return s; + } + + public Sentence farewell() { + Sentence s = webResource.accept(MediaType.APPLICATION_XML).delete(Sentence.class); + return s; + } + + public static void main(String[] args) { + TalkClient remote = new TalkClient("http://localhost:8080/template-server/rest"); + System.out.println(remote.greeting().getWhole()); + System.out.println(remote.farewell().getWhole()); + } +} diff --git a/template-client/src/main/java/com/netflix/template/common/Conversation.java b/template-client/src/main/java/com/netflix/template/common/Conversation.java new file mode 100644 index 0000000000..b85e23e98b --- /dev/null +++ b/template-client/src/main/java/com/netflix/template/common/Conversation.java @@ -0,0 +1,6 @@ +package com.netflix.template.common; + +public interface Conversation { + Sentence greeting(); + Sentence farewell(); +} \ No newline at end of file diff --git a/template-client/src/main/java/com/netflix/template/common/Sentence.java b/template-client/src/main/java/com/netflix/template/common/Sentence.java new file mode 100644 index 0000000000..bf561a6d5a --- /dev/null +++ b/template-client/src/main/java/com/netflix/template/common/Sentence.java @@ -0,0 +1,25 @@ +package com.netflix.template.common; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement +public class Sentence { + private String whole; + + private Sentence() { + }; + + public Sentence(String whole) { + this.whole = whole; + } + + @XmlElement + public String getWhole() { + return whole; + } + + public void setWhole(String whole) { + this.whole = whole; + } +} \ No newline at end of file diff --git a/template-server/.classpath b/template-server/.classpath new file mode 100644 index 0000000000..09505c5c72 --- /dev/null +++ b/template-server/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/template-server/.project b/template-server/.project new file mode 100644 index 0000000000..0b2a3869fa --- /dev/null +++ b/template-server/.project @@ -0,0 +1,19 @@ + + + template-server + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + com.springsource.sts.gradle.core.nature + org.eclipse.jdt.core.javanature + org.eclipse.jdt.groovy.core.groovyNature + + diff --git a/template-server/.settings/gradle/com.springsource.sts.gradle.core.prefs b/template-server/.settings/gradle/com.springsource.sts.gradle.core.prefs new file mode 100644 index 0000000000..1e7f022837 --- /dev/null +++ b/template-server/.settings/gradle/com.springsource.sts.gradle.core.prefs @@ -0,0 +1,4 @@ +#com.springsource.sts.gradle.core.preferences.GradleProjectPreferences +#Sat Mar 17 22:40:30 PDT 2012 +com.springsource.sts.gradle.rootprojectloc=.. +com.springsource.sts.gradle.linkedresources= diff --git a/template-server/.settings/gradle/com.springsource.sts.gradle.refresh.prefs b/template-server/.settings/gradle/com.springsource.sts.gradle.refresh.prefs new file mode 100644 index 0000000000..394fb107f1 --- /dev/null +++ b/template-server/.settings/gradle/com.springsource.sts.gradle.refresh.prefs @@ -0,0 +1,9 @@ +#com.springsource.sts.gradle.core.actions.GradleRefreshPreferences +#Sat Mar 17 22:40:30 PDT 2012 +enableAfterTasks=true +afterTasks=afterEclipseImport; +useHierarchicalNames=false +enableBeforeTasks=true +addResourceFilters=true +enableDSLD=true +beforeTasks=cleanEclipse;eclipse; diff --git a/template-server/bin/com/netflix/template/server/TalkServer.class b/template-server/bin/com/netflix/template/server/TalkServer.class new file mode 100644 index 0000000000000000000000000000000000000000..f534d0627d9bc6892160f0803ec67e0b1aa1bf62 GIT binary patch literal 872 zcmah{%Wl&^6g`tBv2jyEO`p6ArAk>a5@HvGgi0t23n~&tLaZjvG@UY@iR?)l{t8x= zK;i@VD8#i}K?0Uoc5jFUaVL|p7Eba^rc;^n zp3on=h3lcpaP3q~1=qri_}js$jGc!%L#q^lf{8W!z#0O|gj3cq)SoG%+;fJd)_$L% zdSHh#z!H`l@Zd8vBW2{9NivXWPYkqV2qPN{-506K@0Y=hn*hfZ!E-) zQahZ)GNT{0sn8RurYXi_t>OZM&l2rni($94ioewOxIr+lrPemUCT`^oyUnoPDkv{z z(se0S*v>oaAB$9;Q8vTcf~c3BsMG7Tee5uJht>`UpGa5GwUacKuTIZIBU^iPj^GP96*TCq7r_;*kl(mSz*RKq zMhk{j$_mL3$zCVBM$z>TU>PJaRaP9Q;PUvw(c}zsUDW Ykhe;ZE4W|qKPXf$liJ-}afXM#0MazXfdBvi literal 0 HcmV?d00001 diff --git a/template-server/src/main/java/com/netflix/template/server/TalkServer.java b/template-server/src/main/java/com/netflix/template/server/TalkServer.java new file mode 100644 index 0000000000..a856ce88a2 --- /dev/null +++ b/template-server/src/main/java/com/netflix/template/server/TalkServer.java @@ -0,0 +1,26 @@ +package com.netflix.template.server; + +import com.netflix.template.common.Conversation; +import com.netflix.template.common.Sentence; + +import javax.ws.rs.GET; +import javax.ws.rs.DELETE; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +@Path("/talk") +public class TalkServer implements Conversation { + + @GET + @Produces(MediaType.APPLICATION_XML) + public Sentence greeting() { + return new Sentence("Hello"); + } + + @DELETE + @Produces(MediaType.APPLICATION_XML) + public Sentence farewell() { + return new Sentence("Goodbye"); + } +} \ No newline at end of file diff --git a/template-server/src/main/webapp/WEB-INF/web.xml b/template-server/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000..273135a292 --- /dev/null +++ b/template-server/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,25 @@ + + + + Jersey REST Service + + com.sun.jersey.spi.container.servlet.ServletContainer + + + com.sun.jersey.config.property.packages + com.netflix.template.server + + + com.sun.jersey.api.json.POJOMappingFeature + true + + 1 + + + + Jersey REST Service + /rest/* + + From 52bd53f5ea5eec84818d65b40e81d0a82ada6ba8 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Mon, 2 Apr 2012 16:18:03 -0700 Subject: [PATCH 002/179] Restructure into smaller files --- .classpath | 9 - .gitignore | 23 ++ .project | 39 ---- ....springsource.sts.gradle.core.import.prefs | 9 - .../com.springsource.sts.gradle.core.prefs | 4 - .../com.springsource.sts.gradle.refresh.prefs | 9 - LICENSE | 202 ++++++++++++++++++ build.gradle | 24 +-- codequality/HEADER | 13 ++ codequality/checkstyle.xml | 1 + gradle/check.gradle | 2 + gradle/license.gradle | 5 + gradle/local.gradle | 1 + gradle/maven.gradle | 2 +- .../netflix/template/client/TalkClient.class | Bin 2256 -> 0 bytes .../netflix/template/common/Sentence.class | Bin 784 -> 0 bytes .../netflix/template/client/TalkClient.java | 20 +- .../netflix/template/common/Conversation.java | 17 +- .../com/netflix/template/common/Sentence.java | 16 +- 19 files changed, 306 insertions(+), 90 deletions(-) delete mode 100644 .classpath delete mode 100644 .project delete mode 100644 .settings/gradle/com.springsource.sts.gradle.core.import.prefs delete mode 100644 .settings/gradle/com.springsource.sts.gradle.core.prefs delete mode 100644 .settings/gradle/com.springsource.sts.gradle.refresh.prefs create mode 100644 LICENSE create mode 100644 codequality/HEADER create mode 100644 gradle/license.gradle create mode 100644 gradle/local.gradle delete mode 100644 template-client/bin/com/netflix/template/client/TalkClient.class delete mode 100644 template-client/bin/com/netflix/template/common/Sentence.class diff --git a/.classpath b/.classpath deleted file mode 100644 index b1ae8bae1c..0000000000 --- a/.classpath +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/.gitignore b/.gitignore index 618e741f86..313af3cb82 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,26 @@ Thumbs.db # Gradle Files # ################ .gradle + +# Build output directies +/target +*/target +/build +*/build +# +# # IntelliJ specific files/directories +out +.idea +*.ipr +*.iws +*.iml +atlassian-ide-plugin.xml + +# Eclipse specific files/directories +.classpath +.project +.settings +.metadata + +# NetBeans specific files/directories +.nbattrs diff --git a/.project b/.project deleted file mode 100644 index f2d845e45a..0000000000 --- a/.project +++ /dev/null @@ -1,39 +0,0 @@ - - - gradle-template - - - - - - org.eclipse.jdt.core.javabuilder - - - - - - com.springsource.sts.gradle.core.nature - org.eclipse.jdt.core.javanature - org.eclipse.jdt.groovy.core.groovyNature - - - - 1332049227118 - - 10 - - org.eclipse.ui.ide.orFilterMatcher - - - org.eclipse.ui.ide.multiFilter - 1.0-projectRelativePath-equals-true-false-template-server - - - org.eclipse.ui.ide.multiFilter - 1.0-projectRelativePath-equals-true-false-template-client - - - - - - diff --git a/.settings/gradle/com.springsource.sts.gradle.core.import.prefs b/.settings/gradle/com.springsource.sts.gradle.core.import.prefs deleted file mode 100644 index e86c91081f..0000000000 --- a/.settings/gradle/com.springsource.sts.gradle.core.import.prefs +++ /dev/null @@ -1,9 +0,0 @@ -#com.springsource.sts.gradle.core.preferences.GradleImportPreferences -#Sat Mar 17 22:40:13 PDT 2012 -enableAfterTasks=true -afterTasks=afterEclipseImport; -enableDependendencyManagement=true -enableBeforeTasks=true -projects=;template-client;template-server; -enableDSLD=true -beforeTasks=cleanEclipse;eclipse; diff --git a/.settings/gradle/com.springsource.sts.gradle.core.prefs b/.settings/gradle/com.springsource.sts.gradle.core.prefs deleted file mode 100644 index 445ff6da6f..0000000000 --- a/.settings/gradle/com.springsource.sts.gradle.core.prefs +++ /dev/null @@ -1,4 +0,0 @@ -#com.springsource.sts.gradle.core.preferences.GradleProjectPreferences -#Sat Mar 17 22:40:29 PDT 2012 -com.springsource.sts.gradle.rootprojectloc= -com.springsource.sts.gradle.linkedresources= diff --git a/.settings/gradle/com.springsource.sts.gradle.refresh.prefs b/.settings/gradle/com.springsource.sts.gradle.refresh.prefs deleted file mode 100644 index 01e59693e7..0000000000 --- a/.settings/gradle/com.springsource.sts.gradle.refresh.prefs +++ /dev/null @@ -1,9 +0,0 @@ -#com.springsource.sts.gradle.core.actions.GradleRefreshPreferences -#Sat Mar 17 22:40:27 PDT 2012 -enableAfterTasks=true -afterTasks=afterEclipseImport; -useHierarchicalNames=false -enableBeforeTasks=true -addResourceFilters=true -enableDSLD=true -beforeTasks=cleanEclipse;eclipse; diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..7f8ced0d1f --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2012 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. diff --git a/build.gradle b/build.gradle index 5297034a51..9eef3329e1 100644 --- a/build.gradle +++ b/build.gradle @@ -5,18 +5,16 @@ ext.githubProjectName = rootProject.name // TEMPLATE: change to match github pro apply from: file('gradle/convention.gradle') apply from: file('gradle/maven.gradle') apply from: file('gradle/check.gradle') +apply from: file('gradle/license.gradle') -subprojects -{ - group = 'com.netflix' +subprojects { + group = 'com.netflix.osstemplate' // TEMPLATE: Set to organization of project - repositories - { + repositories { mavenCentral() } - dependencies - { + dependencies { compile 'javax.ws.rs:jsr311-api:1.1.1' compile 'com.sun.jersey:jersey-core:1.11' testCompile 'org.testng:testng:6.1.1' @@ -24,21 +22,17 @@ subprojects } } -project(':template-client') -{ - dependencies - { +project(':template-client') { + dependencies { compile 'org.slf4j:slf4j-api:1.6.3' compile 'com.sun.jersey:jersey-client:1.11' } } -project(':template-server') -{ +project(':template-server') { apply plugin: 'war' apply plugin: 'jetty' - dependencies - { + dependencies { compile 'com.sun.jersey:jersey-server:1.11' compile 'com.sun.jersey:jersey-servlet:1.11' compile project(':template-client') diff --git a/codequality/HEADER b/codequality/HEADER new file mode 100644 index 0000000000..b27b192925 --- /dev/null +++ b/codequality/HEADER @@ -0,0 +1,13 @@ + Copyright 2012 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. diff --git a/codequality/checkstyle.xml b/codequality/checkstyle.xml index 3c8a8e6c75..481d2829fd 100644 --- a/codequality/checkstyle.xml +++ b/codequality/checkstyle.xml @@ -50,6 +50,7 @@ + diff --git a/gradle/check.gradle b/gradle/check.gradle index cf6f0461ae..0f80516d45 100644 --- a/gradle/check.gradle +++ b/gradle/check.gradle @@ -9,8 +9,10 @@ subprojects { // FindBugs apply plugin: 'findbugs' + //tasks.withType(Findbugs) { reports.html.enabled true } // PMD apply plugin: 'pmd' + //tasks.withType(Pmd) { reports.html.enabled true } } diff --git a/gradle/license.gradle b/gradle/license.gradle new file mode 100644 index 0000000000..9d04830321 --- /dev/null +++ b/gradle/license.gradle @@ -0,0 +1,5 @@ +buildscript { + dependencies { classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.4' } +} + +apply plugin: 'license' \ No newline at end of file diff --git a/gradle/local.gradle b/gradle/local.gradle new file mode 100644 index 0000000000..6f2d204b8a --- /dev/null +++ b/gradle/local.gradle @@ -0,0 +1 @@ +apply from: 'file://Users/jryan/Workspaces/jryan_build/Tools/nebula-boot/artifactory.gradle' diff --git a/gradle/maven.gradle b/gradle/maven.gradle index 8639564ce4..cb75dfb637 100644 --- a/gradle/maven.gradle +++ b/gradle/maven.gradle @@ -4,7 +4,7 @@ subprojects { apply plugin: 'signing' signing { - required rootProject.performingRelease + required { performingRelease && gradle.taskGraph.hasTask("uploadMavenCentral")} sign configurations.archives } diff --git a/template-client/bin/com/netflix/template/client/TalkClient.class b/template-client/bin/com/netflix/template/client/TalkClient.class deleted file mode 100644 index 90bbaeb3532635ac2638cc61156bb545c24ba039..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2256 zcma)7YgZdp6y2AkOfnq>LJ38x0_72)Fce=%uxhETwQ0cy!B}5IatT8xGjTGZ?Kgjt zx-@jzrOVI$D3|-r3?)3vhndW|x%cd|&)MhPfB*aQZvgYy)zBd@UiNCHD}yb^erg7? zR(GsGnq|k9ZeXri&g13qQ$tdqd&k~P&T}#UzP$B1$DB~bj=-Zk-`9=uBQi zR$%sY*ITv|NZ%}Y)hgq>9a?Ez#v2+24kfertijA17{nDu?ll7vjd zF@Z@9xx-Y#ni!bEw1Bp2IZe5;MU}PJEz(YY@^~qAjABOd(^D`7E|>uYs1~mq6zn@J zkX5fsw<0jp?r>iS#~j9yGH;`J&%pcmKp+)((SCtTtnyKE+}n*04J?AXOZhLwnm-Zn z^y0S*I1Pc13}{FRbQNq@K4{i9rN3rvI@DUG;FT?B-STZ^$BW5e+itM8!jiqZJyom@ z-9&AcInGLHJ8WcukuzivE1TY`zLWuF%NXKbI37jR;zdph>6T?ag}da(3ORNZiTSkV z=(vab8VUkq=Sr4=2Ut;sE|+DUjc_aX=s26RnzJqOqx<#O@Fja%;lGN6jv~I&uttTQ zE8Kku1MApe^HwEe9&6XivST$Ghr}N1UqGkNN!n?Ez6frq{ECB@vVXH6kcaV zI#La+WX-ZUV6!J?Ydr-^U}`%E>WikSbmDA#jbL$MZei}_%%5pUpQGBOH|(LqbI5=LofDw0rkCR?U#@{~aEGBWG!TUAf?aSAP4xT^k6ugf diff --git a/template-client/bin/com/netflix/template/common/Sentence.class b/template-client/bin/com/netflix/template/common/Sentence.class deleted file mode 100644 index 0083f334776617dd596f960095e6ab566be6b905..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 784 zcma)(TT2^36vzLQY%a!Jyhd%G3Z*0|bih6;LMinrR4@{KovhP1VRuH7iRNQjPy`?P z0s5iDb9Q4QUci?**ZrU0Is5<(Z4Lz)E{w>-eFu{T+e)uCd1N31l11u0Zh9o$3;@ zSS+J}qCl-}to}WYdwO`JdZ~;HRn%2O!|^m3_%kyS_|kq4D2-ijyo70X7eJI{=}D1)vPK{;^+21c=PK1?bcIx^)LToG z>Qm)ZiDxhtL#$$rYU`xQah)vd%cRDD*I2%yL<-0)pif?d+nB-aQ8&ZopMj<8ZP4h= VCs6t6dJK?4WvI>*w`N!$fCt$xmcRf2 diff --git a/template-client/src/main/java/com/netflix/template/client/TalkClient.java b/template-client/src/main/java/com/netflix/template/client/TalkClient.java index c3ebf86090..fc9d20d33d 100644 --- a/template-client/src/main/java/com/netflix/template/client/TalkClient.java +++ b/template-client/src/main/java/com/netflix/template/client/TalkClient.java @@ -8,26 +8,42 @@ import javax.ws.rs.core.MediaType; +/** + * Delegates to remote TalkServer over REST. + * @author jryan + * + */ public class TalkClient implements Conversation { - WebResource webResource; + private WebResource webResource; - TalkClient(String location) { + /** + * Instantiate client. + * + * @param location URL to the base of resources, e.g. http://localhost:8080/template-server/rest + */ + public TalkClient(String location) { Client client = Client.create(); client.addFilter(new LoggingFilter(System.out)); webResource = client.resource(location + "/talk"); } + @Override public Sentence greeting() { Sentence s = webResource.accept(MediaType.APPLICATION_XML).get(Sentence.class); return s; } + @Override public Sentence farewell() { Sentence s = webResource.accept(MediaType.APPLICATION_XML).delete(Sentence.class); return s; } + /** + * Tests out client. + * @param args Not applicable + */ public static void main(String[] args) { TalkClient remote = new TalkClient("http://localhost:8080/template-server/rest"); System.out.println(remote.greeting().getWhole()); diff --git a/template-client/src/main/java/com/netflix/template/common/Conversation.java b/template-client/src/main/java/com/netflix/template/common/Conversation.java index b85e23e98b..c190f03bb7 100644 --- a/template-client/src/main/java/com/netflix/template/common/Conversation.java +++ b/template-client/src/main/java/com/netflix/template/common/Conversation.java @@ -1,6 +1,21 @@ package com.netflix.template.common; +/** + * Hold a conversation. + * @author jryan + * + */ public interface Conversation { + + /** + * Initiates a conversation. + * @return Sentence words from geeting + */ Sentence greeting(); + + /** + * End the conversation. + * @return + */ Sentence farewell(); -} \ No newline at end of file +} diff --git a/template-client/src/main/java/com/netflix/template/common/Sentence.java b/template-client/src/main/java/com/netflix/template/common/Sentence.java index bf561a6d5a..616f72efb0 100644 --- a/template-client/src/main/java/com/netflix/template/common/Sentence.java +++ b/template-client/src/main/java/com/netflix/template/common/Sentence.java @@ -3,17 +3,31 @@ import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; +/** + * Container for words going back and forth. + * @author jryan + * + */ @XmlRootElement public class Sentence { private String whole; + @SuppressWarnings("unused") private Sentence() { }; + /** + * Initialize sentence. + * @param whole + */ public Sentence(String whole) { this.whole = whole; } + /** + * whole getter. + * @return + */ @XmlElement public String getWhole() { return whole; @@ -22,4 +36,4 @@ public String getWhole() { public void setWhole(String whole) { this.whole = whole; } -} \ No newline at end of file +} From b5b2f5ef9e908a3c53e4afe017a60f2b878a93b3 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Mon, 9 Apr 2012 10:52:02 -0700 Subject: [PATCH 003/179] Correct artifacts, moved pom to more visible area --- build.gradle | 31 ++++++++++++++++++++++++++----- gradle/convention.gradle | 1 - gradle/license.gradle | 4 ++-- gradle/maven.gradle | 14 ++------------ gradle/release.gradle | 6 ++++++ 5 files changed, 36 insertions(+), 20 deletions(-) create mode 100644 gradle/release.gradle diff --git a/build.gradle b/build.gradle index 9eef3329e1..c0d2d5e7ff 100644 --- a/build.gradle +++ b/build.gradle @@ -2,18 +2,39 @@ ext.releaseVersion = '1.1.3' // TEMPLATE: Set to latest release ext.githubProjectName = rootProject.name // TEMPLATE: change to match github project, if it doesn't match project name +buildscript { + repositories { mavenCentral() } +} + +allprojects { + repositories { mavenCentral() } +} + +//apply from: file('gradle/release.gradle') // Not fully tested apply from: file('gradle/convention.gradle') apply from: file('gradle/maven.gradle') apply from: file('gradle/check.gradle') -apply from: file('gradle/license.gradle') +//apply from: file('gradle/license.gradle') // Waiting for re-release subprojects { - group = 'com.netflix.osstemplate' // TEMPLATE: Set to organization of project - - repositories { - mavenCentral() + // Closure to configure all the POM with extra info, common to all projects + pom { + project { + url "https://github.com/Netflix/${rootProject.githubProjectName}" + scm { + connection "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" + url "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" + developerConnection "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" + } + issueManagement { + system 'github' + url 'https://github.com/Netflix/${rootProject.githubProjectName}/issues' + } + } } + group = 'com.netflix.osstemplate' // TEMPLATE: Set to organization of project + dependencies { compile 'javax.ws.rs:jsr311-api:1.1.1' compile 'com.sun.jersey:jersey-core:1.11' diff --git a/gradle/convention.gradle b/gradle/convention.gradle index a3fc06dd04..9255161209 100644 --- a/gradle/convention.gradle +++ b/gradle/convention.gradle @@ -26,7 +26,6 @@ subprojects } artifacts { - archives jar archives sourcesJar archives javadocJar } diff --git a/gradle/license.gradle b/gradle/license.gradle index 9d04830321..1fdc2702b4 100644 --- a/gradle/license.gradle +++ b/gradle/license.gradle @@ -1,5 +1,5 @@ buildscript { - dependencies { classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.4' } + dependencies { classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.5' } } -apply plugin: 'license' \ No newline at end of file +apply plugin: nl.javadude.gradle.plugins.license.LicensePlugin diff --git a/gradle/maven.gradle b/gradle/maven.gradle index cb75dfb637..ab2792ff33 100644 --- a/gradle/maven.gradle +++ b/gradle/maven.gradle @@ -4,14 +4,14 @@ subprojects { apply plugin: 'signing' signing { - required { performingRelease && gradle.taskGraph.hasTask("uploadMavenCentral")} + required { performingRelease && gradle.taskGraph.hasTask("uploadArchives")} sign configurations.archives } /** * Publishing to Maven Central example provided from http://jedicoder.blogspot.com/2011/11/automated-gradle-project-deployment-to.html */ - task uploadMavenCentral(type:Upload) { + task uploadArchives(type:Upload) { configuration = configurations.archives dependsOn signArchives doFirst { @@ -35,7 +35,6 @@ subprojects { artifactId 'oss-parent' version '7' } - url "https://github.com/Netflix/${rootProject.ext.githubProjectName}" licenses { license { name 'The Apache Software License, Version 2.0' @@ -43,15 +42,6 @@ subprojects { distribution 'repo' } } - scm { - connection "scm:git:git@github.com:Netflix/${rootProject.ext.githubProjectName}.git" - url "scm:git:git@github.com:Netflix/${rootProject.ext.githubProjectName}.git" - developerConnection "scm:git:git@github.com:Netflix/${rootProject.ext.githubProjectName}.git" - } - issueManagement { - system 'github' - url 'https://github.com/Netflix/${rootProject.ext.githubProjectName}/issues' - } } } } diff --git a/gradle/release.gradle b/gradle/release.gradle new file mode 100644 index 0000000000..8fc34dbff6 --- /dev/null +++ b/gradle/release.gradle @@ -0,0 +1,6 @@ +buildscript { + dependencies { classpath group: 'no.entitas.gradle', name: 'gradle-release-plugin', version: '1.11' } +} + +apply plugin: no.entitas.gradle.git.GitReleasePlugin // 'gitrelease' + From 9fa9ec0acce8afc01da943b1922a731059cb4cd2 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Mon, 9 Apr 2012 15:06:15 -0700 Subject: [PATCH 004/179] Avoid signatures in archives unless doing mavenCentral build --- build.gradle | 21 ++++++++++----------- gradle/maven.gradle | 14 +++++++++----- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/build.gradle b/build.gradle index c0d2d5e7ff..0fc71a5055 100644 --- a/build.gradle +++ b/build.gradle @@ -20,16 +20,16 @@ subprojects { // Closure to configure all the POM with extra info, common to all projects pom { project { - url "https://github.com/Netflix/${rootProject.githubProjectName}" - scm { - connection "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" - url "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" - developerConnection "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" - } - issueManagement { - system 'github' - url 'https://github.com/Netflix/${rootProject.githubProjectName}/issues' - } + url "https://github.com/Netflix/${rootProject.githubProjectName}" + scm { + connection "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" + url "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" + developerConnection "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" + } + issueManagement { + system 'github' + url 'https://github.com/Netflix/${rootProject.githubProjectName}/issues' + } } } @@ -57,7 +57,6 @@ project(':template-server') { compile 'com.sun.jersey:jersey-server:1.11' compile 'com.sun.jersey:jersey-servlet:1.11' compile project(':template-client') - testCompile 'org.mockito:mockito-core:1.8.5' } } diff --git a/gradle/maven.gradle b/gradle/maven.gradle index ab2792ff33..3de0990466 100644 --- a/gradle/maven.gradle +++ b/gradle/maven.gradle @@ -3,17 +3,21 @@ subprojects { apply plugin: 'maven' // Java plugin has to have been already applied for the conf2scope mappings to work apply plugin: 'signing' - signing { - required { performingRelease && gradle.taskGraph.hasTask("uploadArchives")} - sign configurations.archives + gradle.taskGraph.whenReady { taskGraph -> + if (taskGraph.hasTask("uploadMavenCentral")) { + signing { + required true + sign configurations.archives + } + } } /** * Publishing to Maven Central example provided from http://jedicoder.blogspot.com/2011/11/automated-gradle-project-deployment-to.html */ - task uploadArchives(type:Upload) { + task uploadMavenCentral(type:Upload) { configuration = configurations.archives - dependsOn signArchives + dependsOn 'signArchives' doFirst { repositories.mavenDeployer { beforeDeployment { org.gradle.api.artifacts.maven.MavenDeployment deployment -> signing.signPom(deployment) } From 3a10a077f9fc6c3aff7b7b1eebe9365d34b5acb4 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Mon, 9 Apr 2012 15:30:00 -0700 Subject: [PATCH 005/179] Remove local testing file --- gradle/local.gradle | 1 - 1 file changed, 1 deletion(-) delete mode 100644 gradle/local.gradle diff --git a/gradle/local.gradle b/gradle/local.gradle deleted file mode 100644 index 6f2d204b8a..0000000000 --- a/gradle/local.gradle +++ /dev/null @@ -1 +0,0 @@ -apply from: 'file://Users/jryan/Workspaces/jryan_build/Tools/nebula-boot/artifactory.gradle' From 61b1710621d138556fe2d5621076ea40ad47f8af Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Mon, 9 Apr 2012 15:56:00 -0700 Subject: [PATCH 006/179] Multimodule builds need a dump signing task --- gradle/maven.gradle | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/gradle/maven.gradle b/gradle/maven.gradle index 3de0990466..1673a24f8c 100644 --- a/gradle/maven.gradle +++ b/gradle/maven.gradle @@ -9,6 +9,10 @@ subprojects { required true sign configurations.archives } + } else { + task signArchives { + // do nothing + } } } @@ -17,7 +21,7 @@ subprojects { */ task uploadMavenCentral(type:Upload) { configuration = configurations.archives - dependsOn 'signArchives' + dependsOn { 'signArchives' } doFirst { repositories.mavenDeployer { beforeDeployment { org.gradle.api.artifacts.maven.MavenDeployment deployment -> signing.signPom(deployment) } From 66332d8b8fe98c8068c85c215dbbacc66397a68f Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Mon, 9 Apr 2012 16:00:55 -0700 Subject: [PATCH 007/179] Fix quotes --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0fc71a5055..55e5eeb557 100644 --- a/build.gradle +++ b/build.gradle @@ -28,7 +28,7 @@ subprojects { } issueManagement { system 'github' - url 'https://github.com/Netflix/${rootProject.githubProjectName}/issues' + url "https://github.com/Netflix/${rootProject.githubProjectName}/issues" } } } From 1df6e445488edfec78512c70e4db7352a1df57ec Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Mon, 9 Apr 2012 17:04:28 -0700 Subject: [PATCH 008/179] Use lifecycle to add signing task --- gradle/maven.gradle | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/gradle/maven.gradle b/gradle/maven.gradle index 1673a24f8c..560e66b4d4 100644 --- a/gradle/maven.gradle +++ b/gradle/maven.gradle @@ -3,16 +3,14 @@ subprojects { apply plugin: 'maven' // Java plugin has to have been already applied for the conf2scope mappings to work apply plugin: 'signing' - gradle.taskGraph.whenReady { taskGraph -> - if (taskGraph.hasTask("uploadMavenCentral")) { - signing { - required true - sign configurations.archives - } - } else { - task signArchives { - // do nothing - } + if (gradle.startParameter.taskNames.contains("uploadMavenCentral")) { + signing { + required true + sign configurations.archives + } + } else { + task signArchives { + // do nothing } } @@ -21,7 +19,7 @@ subprojects { */ task uploadMavenCentral(type:Upload) { configuration = configurations.archives - dependsOn { 'signArchives' } + dependsOn 'signArchives' doFirst { repositories.mavenDeployer { beforeDeployment { org.gradle.api.artifacts.maven.MavenDeployment deployment -> signing.signPom(deployment) } From 7c28a7637fbaf8c78ee8efcdae85592663960ea6 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Mon, 9 Apr 2012 19:10:02 -0700 Subject: [PATCH 009/179] Create branch that contains only build related files --- template-client/.classpath | 11 ---- template-client/.project | 19 ------- .../com.springsource.sts.gradle.core.prefs | 4 -- .../com.springsource.sts.gradle.refresh.prefs | 9 --- .../template/common/Conversation.class | Bin 214 -> 0 bytes .../netflix/template/client/TalkClient.java | 52 ------------------ .../netflix/template/common/Conversation.java | 21 ------- .../com/netflix/template/common/Sentence.java | 39 ------------- template-server/.classpath | 12 ---- template-server/.project | 19 ------- .../com.springsource.sts.gradle.core.prefs | 4 -- .../com.springsource.sts.gradle.refresh.prefs | 9 --- .../netflix/template/server/TalkServer.class | Bin 872 -> 0 bytes .../netflix/template/server/TalkServer.java | 26 --------- .../src/main/webapp/WEB-INF/web.xml | 25 --------- 15 files changed, 250 deletions(-) delete mode 100644 template-client/.classpath delete mode 100644 template-client/.project delete mode 100644 template-client/.settings/gradle/com.springsource.sts.gradle.core.prefs delete mode 100644 template-client/.settings/gradle/com.springsource.sts.gradle.refresh.prefs delete mode 100644 template-client/bin/com/netflix/template/common/Conversation.class delete mode 100644 template-client/src/main/java/com/netflix/template/client/TalkClient.java delete mode 100644 template-client/src/main/java/com/netflix/template/common/Conversation.java delete mode 100644 template-client/src/main/java/com/netflix/template/common/Sentence.java delete mode 100644 template-server/.classpath delete mode 100644 template-server/.project delete mode 100644 template-server/.settings/gradle/com.springsource.sts.gradle.core.prefs delete mode 100644 template-server/.settings/gradle/com.springsource.sts.gradle.refresh.prefs delete mode 100644 template-server/bin/com/netflix/template/server/TalkServer.class delete mode 100644 template-server/src/main/java/com/netflix/template/server/TalkServer.java delete mode 100644 template-server/src/main/webapp/WEB-INF/web.xml diff --git a/template-client/.classpath b/template-client/.classpath deleted file mode 100644 index cd1d475d6b..0000000000 --- a/template-client/.classpath +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/template-client/.project b/template-client/.project deleted file mode 100644 index 66279e9c19..0000000000 --- a/template-client/.project +++ /dev/null @@ -1,19 +0,0 @@ - - - template-client - - - - - - org.eclipse.jdt.core.javabuilder - - - - - - com.springsource.sts.gradle.core.nature - org.eclipse.jdt.core.javanature - org.eclipse.jdt.groovy.core.groovyNature - - diff --git a/template-client/.settings/gradle/com.springsource.sts.gradle.core.prefs b/template-client/.settings/gradle/com.springsource.sts.gradle.core.prefs deleted file mode 100644 index 1e7f022837..0000000000 --- a/template-client/.settings/gradle/com.springsource.sts.gradle.core.prefs +++ /dev/null @@ -1,4 +0,0 @@ -#com.springsource.sts.gradle.core.preferences.GradleProjectPreferences -#Sat Mar 17 22:40:30 PDT 2012 -com.springsource.sts.gradle.rootprojectloc=.. -com.springsource.sts.gradle.linkedresources= diff --git a/template-client/.settings/gradle/com.springsource.sts.gradle.refresh.prefs b/template-client/.settings/gradle/com.springsource.sts.gradle.refresh.prefs deleted file mode 100644 index 394fb107f1..0000000000 --- a/template-client/.settings/gradle/com.springsource.sts.gradle.refresh.prefs +++ /dev/null @@ -1,9 +0,0 @@ -#com.springsource.sts.gradle.core.actions.GradleRefreshPreferences -#Sat Mar 17 22:40:30 PDT 2012 -enableAfterTasks=true -afterTasks=afterEclipseImport; -useHierarchicalNames=false -enableBeforeTasks=true -addResourceFilters=true -enableDSLD=true -beforeTasks=cleanEclipse;eclipse; diff --git a/template-client/bin/com/netflix/template/common/Conversation.class b/template-client/bin/com/netflix/template/common/Conversation.class deleted file mode 100644 index 86d42f57590c9fbee4a60450fd6eca4121b4cf6a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 214 zcmaKmF%H5o6hoZ?ZRrHJC`%);G9ob{G4uo>`mIu>KPZI4*%&wghe9M96O$!dw%_~n zd;!>^Dv$}(+KrMabk;m%pz&f=AQ{ckvD`bJ$X``3jtk5MR)d<9w2FIqIuE3SK-qhu zV7QN4_2&3*t|bn{ns%|(DNlE@R-kI#&1*UsO9JcP%O<_$0s^y03}lgDfgFjXNE(we H`B;7deJVPB diff --git a/template-client/src/main/java/com/netflix/template/client/TalkClient.java b/template-client/src/main/java/com/netflix/template/client/TalkClient.java deleted file mode 100644 index fc9d20d33d..0000000000 --- a/template-client/src/main/java/com/netflix/template/client/TalkClient.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.netflix.template.client; - -import com.netflix.template.common.Conversation; -import com.netflix.template.common.Sentence; -import com.sun.jersey.api.client.Client; -import com.sun.jersey.api.client.WebResource; -import com.sun.jersey.api.client.filter.LoggingFilter; - -import javax.ws.rs.core.MediaType; - -/** - * Delegates to remote TalkServer over REST. - * @author jryan - * - */ -public class TalkClient implements Conversation { - - private WebResource webResource; - - /** - * Instantiate client. - * - * @param location URL to the base of resources, e.g. http://localhost:8080/template-server/rest - */ - public TalkClient(String location) { - Client client = Client.create(); - client.addFilter(new LoggingFilter(System.out)); - webResource = client.resource(location + "/talk"); - } - - @Override - public Sentence greeting() { - Sentence s = webResource.accept(MediaType.APPLICATION_XML).get(Sentence.class); - return s; - } - - @Override - public Sentence farewell() { - Sentence s = webResource.accept(MediaType.APPLICATION_XML).delete(Sentence.class); - return s; - } - - /** - * Tests out client. - * @param args Not applicable - */ - public static void main(String[] args) { - TalkClient remote = new TalkClient("http://localhost:8080/template-server/rest"); - System.out.println(remote.greeting().getWhole()); - System.out.println(remote.farewell().getWhole()); - } -} diff --git a/template-client/src/main/java/com/netflix/template/common/Conversation.java b/template-client/src/main/java/com/netflix/template/common/Conversation.java deleted file mode 100644 index c190f03bb7..0000000000 --- a/template-client/src/main/java/com/netflix/template/common/Conversation.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.netflix.template.common; - -/** - * Hold a conversation. - * @author jryan - * - */ -public interface Conversation { - - /** - * Initiates a conversation. - * @return Sentence words from geeting - */ - Sentence greeting(); - - /** - * End the conversation. - * @return - */ - Sentence farewell(); -} diff --git a/template-client/src/main/java/com/netflix/template/common/Sentence.java b/template-client/src/main/java/com/netflix/template/common/Sentence.java deleted file mode 100644 index 616f72efb0..0000000000 --- a/template-client/src/main/java/com/netflix/template/common/Sentence.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.netflix.template.common; - -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; - -/** - * Container for words going back and forth. - * @author jryan - * - */ -@XmlRootElement -public class Sentence { - private String whole; - - @SuppressWarnings("unused") - private Sentence() { - }; - - /** - * Initialize sentence. - * @param whole - */ - public Sentence(String whole) { - this.whole = whole; - } - - /** - * whole getter. - * @return - */ - @XmlElement - public String getWhole() { - return whole; - } - - public void setWhole(String whole) { - this.whole = whole; - } -} diff --git a/template-server/.classpath b/template-server/.classpath deleted file mode 100644 index 09505c5c72..0000000000 --- a/template-server/.classpath +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/template-server/.project b/template-server/.project deleted file mode 100644 index 0b2a3869fa..0000000000 --- a/template-server/.project +++ /dev/null @@ -1,19 +0,0 @@ - - - template-server - - - - - - org.eclipse.jdt.core.javabuilder - - - - - - com.springsource.sts.gradle.core.nature - org.eclipse.jdt.core.javanature - org.eclipse.jdt.groovy.core.groovyNature - - diff --git a/template-server/.settings/gradle/com.springsource.sts.gradle.core.prefs b/template-server/.settings/gradle/com.springsource.sts.gradle.core.prefs deleted file mode 100644 index 1e7f022837..0000000000 --- a/template-server/.settings/gradle/com.springsource.sts.gradle.core.prefs +++ /dev/null @@ -1,4 +0,0 @@ -#com.springsource.sts.gradle.core.preferences.GradleProjectPreferences -#Sat Mar 17 22:40:30 PDT 2012 -com.springsource.sts.gradle.rootprojectloc=.. -com.springsource.sts.gradle.linkedresources= diff --git a/template-server/.settings/gradle/com.springsource.sts.gradle.refresh.prefs b/template-server/.settings/gradle/com.springsource.sts.gradle.refresh.prefs deleted file mode 100644 index 394fb107f1..0000000000 --- a/template-server/.settings/gradle/com.springsource.sts.gradle.refresh.prefs +++ /dev/null @@ -1,9 +0,0 @@ -#com.springsource.sts.gradle.core.actions.GradleRefreshPreferences -#Sat Mar 17 22:40:30 PDT 2012 -enableAfterTasks=true -afterTasks=afterEclipseImport; -useHierarchicalNames=false -enableBeforeTasks=true -addResourceFilters=true -enableDSLD=true -beforeTasks=cleanEclipse;eclipse; diff --git a/template-server/bin/com/netflix/template/server/TalkServer.class b/template-server/bin/com/netflix/template/server/TalkServer.class deleted file mode 100644 index f534d0627d9bc6892160f0803ec67e0b1aa1bf62..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 872 zcmah{%Wl&^6g`tBv2jyEO`p6ArAk>a5@HvGgi0t23n~&tLaZjvG@UY@iR?)l{t8x= zK;i@VD8#i}K?0Uoc5jFUaVL|p7Eba^rc;^n zp3on=h3lcpaP3q~1=qri_}js$jGc!%L#q^lf{8W!z#0O|gj3cq)SoG%+;fJd)_$L% zdSHh#z!H`l@Zd8vBW2{9NivXWPYkqV2qPN{-506K@0Y=hn*hfZ!E-) zQahZ)GNT{0sn8RurYXi_t>OZM&l2rni($94ioewOxIr+lrPemUCT`^oyUnoPDkv{z z(se0S*v>oaAB$9;Q8vTcf~c3BsMG7Tee5uJht>`UpGa5GwUacKuTIZIBU^iPj^GP96*TCq7r_;*kl(mSz*RKq zMhk{j$_mL3$zCVBM$z>TU>PJaRaP9Q;PUvw(c}zsUDW Ykhe;ZE4W|qKPXf$liJ-}afXM#0MazXfdBvi diff --git a/template-server/src/main/java/com/netflix/template/server/TalkServer.java b/template-server/src/main/java/com/netflix/template/server/TalkServer.java deleted file mode 100644 index a856ce88a2..0000000000 --- a/template-server/src/main/java/com/netflix/template/server/TalkServer.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.netflix.template.server; - -import com.netflix.template.common.Conversation; -import com.netflix.template.common.Sentence; - -import javax.ws.rs.GET; -import javax.ws.rs.DELETE; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; - -@Path("/talk") -public class TalkServer implements Conversation { - - @GET - @Produces(MediaType.APPLICATION_XML) - public Sentence greeting() { - return new Sentence("Hello"); - } - - @DELETE - @Produces(MediaType.APPLICATION_XML) - public Sentence farewell() { - return new Sentence("Goodbye"); - } -} \ No newline at end of file diff --git a/template-server/src/main/webapp/WEB-INF/web.xml b/template-server/src/main/webapp/WEB-INF/web.xml deleted file mode 100644 index 273135a292..0000000000 --- a/template-server/src/main/webapp/WEB-INF/web.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - Jersey REST Service - - com.sun.jersey.spi.container.servlet.ServletContainer - - - com.sun.jersey.config.property.packages - com.netflix.template.server - - - com.sun.jersey.api.json.POJOMappingFeature - true - - 1 - - - - Jersey REST Service - /rest/* - - From bc662051d8c72ea7b20350b1746e1a8f527c9244 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Mon, 9 Apr 2012 19:14:47 -0700 Subject: [PATCH 010/179] Un-indenting HEADER --- codequality/HEADER | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/codequality/HEADER b/codequality/HEADER index b27b192925..6c5c7c9c77 100644 --- a/codequality/HEADER +++ b/codequality/HEADER @@ -1,13 +1,13 @@ - Copyright 2012 Netflix, Inc. +Copyright 2012 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 +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 + 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. +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. From bf5b268244d4eea430049627a734b3fa19307b3e Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Fri, 13 Apr 2012 15:40:46 -0700 Subject: [PATCH 011/179] Make one less thing people have to change --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 55e5eeb557..db70396359 100644 --- a/build.gradle +++ b/build.gradle @@ -33,7 +33,7 @@ subprojects { } } - group = 'com.netflix.osstemplate' // TEMPLATE: Set to organization of project + group = "com.netflix.${githubProjectName}" // TEMPLATE: Set to organization of project dependencies { compile 'javax.ws.rs:jsr311-api:1.1.1' From eaa8fc97a4427d51bf8637570a263d4f4e33e5af Mon Sep 17 00:00:00 2001 From: Jordan Zimmerman Date: Fri, 3 Aug 2012 11:59:35 -0700 Subject: [PATCH 012/179] Sonatype URL was wrong --- gradle/maven.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/maven.gradle b/gradle/maven.gradle index 560e66b4d4..7efb83333b 100644 --- a/gradle/maven.gradle +++ b/gradle/maven.gradle @@ -27,7 +27,7 @@ subprojects { // To test deployment locally, use the following instead of oss.sonatype.org //repository(url: "file://localhost/${rootProject.rootDir}/repo") - repository(url: 'http://oss.sonatype.org/services/local/staging/deply/maven2/') { + repository(url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2') { authentication(userName: rootProject.sonatypeUsername, password: rootProject.sonatypePassword) } From 0f0c6114889de1c0c3d702661aa44f95ec76edea Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Fri, 17 Aug 2012 16:02:17 -0700 Subject: [PATCH 013/179] Enable license header plugin --- build.gradle | 4 +++- codequality/HEADER | 2 +- gradle/buildscript.gradle | 3 +++ gradle/license.gradle | 12 ++++++++---- 4 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 gradle/buildscript.gradle diff --git a/build.gradle b/build.gradle index db70396359..fae039b42b 100644 --- a/build.gradle +++ b/build.gradle @@ -4,6 +4,7 @@ ext.githubProjectName = rootProject.name // TEMPLATE: change to match github pro buildscript { repositories { mavenCentral() } + apply from: file('gradle/buildscript.gradle'), to: buildscript } allprojects { @@ -14,7 +15,7 @@ allprojects { apply from: file('gradle/convention.gradle') apply from: file('gradle/maven.gradle') apply from: file('gradle/check.gradle') -//apply from: file('gradle/license.gradle') // Waiting for re-release +apply from: file('gradle/license.gradle') subprojects { // Closure to configure all the POM with extra info, common to all projects @@ -44,6 +45,7 @@ subprojects { } project(':template-client') { + apply plugin: 'java' dependencies { compile 'org.slf4j:slf4j-api:1.6.3' compile 'com.sun.jersey:jersey-client:1.11' diff --git a/codequality/HEADER b/codequality/HEADER index 6c5c7c9c77..3102e4b449 100644 --- a/codequality/HEADER +++ b/codequality/HEADER @@ -1,4 +1,4 @@ -Copyright 2012 Netflix, Inc. +Copyright ${year} Netflix, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/gradle/buildscript.gradle b/gradle/buildscript.gradle new file mode 100644 index 0000000000..77d13d1026 --- /dev/null +++ b/gradle/buildscript.gradle @@ -0,0 +1,3 @@ +// Executed in context of buildscript +dependencies { classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.6.0' } + diff --git a/gradle/license.gradle b/gradle/license.gradle index 1fdc2702b4..11a51f1137 100644 --- a/gradle/license.gradle +++ b/gradle/license.gradle @@ -1,5 +1,9 @@ -buildscript { - dependencies { classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.5' } -} +// Dependency for plugin was set in buildscript.gradle -apply plugin: nl.javadude.gradle.plugins.license.LicensePlugin +subprojects { +apply plugin: 'license' //nl.javadude.gradle.plugins.license.LicensePlugin +license { + header rootProject.file('codequality/HEADER') + ext.year = Calendar.getInstance().get(Calendar.YEAR) +} +} From c2af08e723518a2191e51bf9eae694fda12d1bbc Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Fri, 17 Aug 2012 16:02:31 -0700 Subject: [PATCH 014/179] Upgrade to Gradle 1.1. --- gradle/convention.gradle | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 39752 -> 45502 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 4 +- gradlew.bat | 180 +++++++++++------------ 5 files changed, 95 insertions(+), 95 deletions(-) diff --git a/gradle/convention.gradle b/gradle/convention.gradle index 9255161209..919e382901 100644 --- a/gradle/convention.gradle +++ b/gradle/convention.gradle @@ -40,5 +40,5 @@ task aggregateJavadoc(type: Javadoc) { // Generate wrapper, which is distributed as part of source to alleviate the need of installing gradle task createWrapper(type: Wrapper) { - gradleVersion = '1.0-milestone-9' + gradleVersion = '1.1' } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 2cb758a1c693a7cc7a489e4450ca1c700ac83049..7f1e239c8466c730b569575c494a89e6bb4c416a 100644 GIT binary patch delta 40518 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3798e7cf1c..9829a99a5b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Mar 14 13:19:56 PDT 2012 +#Tue Aug 14 16:28:54 PDT 2012 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=http\://services.gradle.org/distributions/gradle-1.0-milestone-9-bin.zip +distributionUrl=http\://services.gradle.org/distributions/gradle-1.1-bin.zip diff --git a/gradlew b/gradlew index ae91ed9029..e61422d06d 100755 --- a/gradlew +++ b/gradlew @@ -101,13 +101,13 @@ if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else - warn "Could not query businessSystem maximum file descriptor limit: $MAX_FD_LIMIT" + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then - JAVA_OPTS="$JAVA_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java diff --git a/gradlew.bat b/gradlew.bat index 8a0b282aa6..aec99730b4 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,90 +1,90 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega From a85e1963ffaacd0d695baa356f69adccfcbbebf4 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Mon, 20 Aug 2012 14:26:35 -0700 Subject: [PATCH 015/179] Adding cobertura --- gradle/buildscript.gradle | 6 ++++-- gradle/check.gradle | 7 +++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/gradle/buildscript.gradle b/gradle/buildscript.gradle index 77d13d1026..328acfc314 100644 --- a/gradle/buildscript.gradle +++ b/gradle/buildscript.gradle @@ -1,3 +1,5 @@ // Executed in context of buildscript -dependencies { classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.6.0' } - +dependencies { + classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.6.0' + classpath 'com.mapvine:gradle-cobertura-plugin:0.1' +} diff --git a/gradle/check.gradle b/gradle/check.gradle index 0f80516d45..7617f17b35 100644 --- a/gradle/check.gradle +++ b/gradle/check.gradle @@ -15,4 +15,11 @@ subprojects { apply plugin: 'pmd' //tasks.withType(Pmd) { reports.html.enabled true } + apply plugin: 'cobertura' + cobertura { + sourceDirs = sourceSets.main.java.srcDirs + format = 'html' + includes = ['**/*.java', '**/*.groovy'] + excludes = [] + } } From 8f289b73e539edc34a4fcd22df1c6dc9d97f216f Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Mon, 20 Aug 2012 23:34:49 -0700 Subject: [PATCH 016/179] Release plugin --- build.gradle | 5 ++- gradle.properties | 1 + gradle/buildscript.gradle | 7 +++++ gradle/convention.gradle | 11 +++---- gradle/maven.gradle | 15 +++------ gradle/release.gradle | 64 +++++++++++++++++++++++++++++++++++++-- 6 files changed, 79 insertions(+), 24 deletions(-) create mode 100644 gradle.properties diff --git a/build.gradle b/build.gradle index fae039b42b..65b498ec84 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,5 @@ // Establish version and status -ext.releaseVersion = '1.1.3' // TEMPLATE: Set to latest release -ext.githubProjectName = rootProject.name // TEMPLATE: change to match github project, if it doesn't match project name +ext.githubProjectName = rootProject.name // Change if github project name is not the same as the root project's name buildscript { repositories { mavenCentral() } @@ -11,11 +10,11 @@ allprojects { repositories { mavenCentral() } } -//apply from: file('gradle/release.gradle') // Not fully tested apply from: file('gradle/convention.gradle') apply from: file('gradle/maven.gradle') apply from: file('gradle/check.gradle') apply from: file('gradle/license.gradle') +apply from: file('gradle/release.gradle') subprojects { // Closure to configure all the POM with extra info, common to all projects diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000000..6b59bf6c43 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +version=1.4-SNAPSHOT diff --git a/gradle/buildscript.gradle b/gradle/buildscript.gradle index 328acfc314..59ffb3d33c 100644 --- a/gradle/buildscript.gradle +++ b/gradle/buildscript.gradle @@ -1,5 +1,12 @@ // Executed in context of buildscript +repositories { + ivy { + name = 'gradle_release' + artifactPattern 'http://launchpad.net/[organization]/trunk/[revision]/+download/[artifact]-[revision].jar' + } +} dependencies { classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.6.0' classpath 'com.mapvine:gradle-cobertura-plugin:0.1' + classpath 'gradle-release:gradle-release:1.0pre' } diff --git a/gradle/convention.gradle b/gradle/convention.gradle index 919e382901..65da4e30df 100644 --- a/gradle/convention.gradle +++ b/gradle/convention.gradle @@ -1,11 +1,8 @@ -ext.performingRelease = project.hasProperty('release') && Boolean.parseBoolean(project.release) -def versionPostfix = performingRelease?'':'-SNAPSHOT' -version = "${releaseVersion}${versionPostfix}" -status = performingRelease?'release':'snapshot' +// For Artifactory +rootProject.status = version.contains('-SNAPSHOT')?'snapshot':'release' -subprojects -{ +subprojects { project -> apply plugin: 'java' // Plugin as major conventions version = rootProject.version @@ -13,7 +10,7 @@ subprojects sourceCompatibility = 1.6 // GRADLE-2087 workaround, perform after java plugin - status = rootProject.status + status = version.contains('-SNAPSHOT')?'snapshot':'release' task sourcesJar(type: Jar, dependsOn:classes) { classifier = 'sources' diff --git a/gradle/maven.gradle b/gradle/maven.gradle index 7efb83333b..29f5d405d2 100644 --- a/gradle/maven.gradle +++ b/gradle/maven.gradle @@ -3,23 +3,16 @@ subprojects { apply plugin: 'maven' // Java plugin has to have been already applied for the conf2scope mappings to work apply plugin: 'signing' - if (gradle.startParameter.taskNames.contains("uploadMavenCentral")) { - signing { - required true - sign configurations.archives - } - } else { - task signArchives { - // do nothing - } + signing { + required { gradle.taskGraph.hasTask(uploadMavenCentral) } + sign configurations.archives } /** * Publishing to Maven Central example provided from http://jedicoder.blogspot.com/2011/11/automated-gradle-project-deployment-to.html */ - task uploadMavenCentral(type:Upload) { + task uploadMavenCentral(type:Upload, dependsOn: signArchives) { configuration = configurations.archives - dependsOn 'signArchives' doFirst { repositories.mavenDeployer { beforeDeployment { org.gradle.api.artifacts.maven.MavenDeployment deployment -> signing.signPom(deployment) } diff --git a/gradle/release.gradle b/gradle/release.gradle index 8fc34dbff6..fe4bc2ebdf 100644 --- a/gradle/release.gradle +++ b/gradle/release.gradle @@ -1,6 +1,64 @@ -buildscript { - dependencies { classpath group: 'no.entitas.gradle', name: 'gradle-release-plugin', version: '1.11' } + +apply plugin: 'release' + +// Ignore release plugin's task because it calls out via GradleBuild. This is a good place to put an email to send out +task release(overwrite: true, dependsOn: commitNewVersion) << { + // This is a good place to put an email to send out +} +commitNewVersion.dependsOn updateVersion +updateVersion.dependsOn createReleaseTag +createReleaseTag.dependsOn preTagCommit +def buildTasks = tasks.matching { it.name =~ /:build/ } +preTagCommit.dependsOn buildTasks +preTagCommit.dependsOn checkSnapshotDependencies +//checkSnapshotDependencies.dependsOn confirmReleaseVersion // Introduced in 1.0, forces readLine +//confirmReleaseVersion.dependsOn unSnapshotVersion +checkSnapshotDependencies.dependsOn unSnapshotVersion // Remove once above is fixed +unSnapshotVersion.dependsOn checkUpdateNeeded +checkUpdateNeeded.dependsOn checkCommitNeeded +checkCommitNeeded.dependsOn initScmPlugin + +// Call out to compile against internal repository +task uploadArtifactory(type: GradleBuild) { + startParameter = project.gradle.startParameter.newInstance() + startParameter.addInitScript( file('gradle/netflix-oss.gradle') ) + startParameter.getExcludedTaskNames().add('check') + tasks = [ 'build', 'artifactoryPublish' ] } -apply plugin: no.entitas.gradle.git.GitReleasePlugin // 'gitrelease' +task buildWithArtifactory(type: GradleBuild) { + startParameter = project.gradle.startParameter.newInstance() + startParameter.addInitScript( file('gradle/netflix-oss.gradle') ) + startParameter.getExcludedTaskNames().add('check') + tasks = [ 'build' ] +} +// Ensure upload happens before taggging but after all pre-checks +uploadArtifactory.dependsOn checkSnapshotDependencies +createReleaseTag.dependsOn uploadArtifactory +gradle.taskGraph.whenReady { taskGraph -> + if ( taskGraph.hasTask(uploadArtifactory) && !taskGraph.hasTask(':release') ) { + throw new GradleException('"release" task has to be run before uploading to Artifactory') + } +} +subprojects.each { project -> + project.uploadMavenCentral.dependsOn rootProject.checkSnapshotDependencies + rootProject.createReleaseTag.dependsOn project.uploadMavenCentral + + gradle.taskGraph.whenReady { taskGraph -> + if ( taskGraph.hasTask(project.uploadMavenCentral) && !taskGraph.hasTask(':release') ) { + throw new GradleException('"release" task has to be run before uploading to Maven Central') + } + } +} + +// Prevent plugin from asking for a version number interactively +ext.'gradle.release.useAutomaticVersion' = "true" + +release { + // http://tellurianring.com/wiki/gradle/release + failOnCommitNeeded=false + failOnPublishNeeded=false + failOnUnversionedFiles=false + failOnUpdateNeeded=false +} From ddedbd72afbfbd786614983f3ffbc10e1522c6ec Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Fri, 31 Aug 2012 15:35:27 -0700 Subject: [PATCH 017/179] Pointing to a repo in our control --- gradle/buildscript.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/buildscript.gradle b/gradle/buildscript.gradle index 59ffb3d33c..d12c78383a 100644 --- a/gradle/buildscript.gradle +++ b/gradle/buildscript.gradle @@ -1,8 +1,8 @@ // Executed in context of buildscript repositories { - ivy { - name = 'gradle_release' - artifactPattern 'http://launchpad.net/[organization]/trunk/[revision]/+download/[artifact]-[revision].jar' + maven { + name 'build-repo' + url 'https://github.com/Netflix-Skunkworks/build-repo/raw/master/releases/' } } dependencies { From f170238c6df0e5991758ea14058e1b6ef05fa905 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Tue, 4 Sep 2012 11:46:58 -0700 Subject: [PATCH 018/179] Using custom build of release plugin, to support building from a branch --- gradle/buildscript.gradle | 2 +- gradle/release.gradle | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/gradle/buildscript.gradle b/gradle/buildscript.gradle index d12c78383a..c63c13006f 100644 --- a/gradle/buildscript.gradle +++ b/gradle/buildscript.gradle @@ -8,5 +8,5 @@ repositories { dependencies { classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.6.0' classpath 'com.mapvine:gradle-cobertura-plugin:0.1' - classpath 'gradle-release:gradle-release:1.0pre' + classpath 'gradle-release:gradle-release:1.0-SNAPSHOT' } diff --git a/gradle/release.gradle b/gradle/release.gradle index fe4bc2ebdf..8ed0305107 100644 --- a/gradle/release.gradle +++ b/gradle/release.gradle @@ -57,8 +57,9 @@ ext.'gradle.release.useAutomaticVersion' = "true" release { // http://tellurianring.com/wiki/gradle/release - failOnCommitNeeded=false - failOnPublishNeeded=false - failOnUnversionedFiles=false - failOnUpdateNeeded=false + failOnCommitNeeded=true + failOnPublishNeeded=true + failOnUnversionedFiles=true + failOnUpdateNeeded=true + requireBranch = null } From 1954d730193a6ee7300cbcdf5f4cfaa74e9faa3e Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Wed, 5 Sep 2012 16:33:00 -0700 Subject: [PATCH 019/179] Setting default name for multi-project --- settings.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/settings.gradle b/settings.gradle index 350f2f1b49..5dd25eb8c6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,2 @@ +rootProject.name='gradle-template-multi' // TEMPLATE: Change this include 'template-client','template-server' From 2b31d36a03edcc93a3d2c478ba8b8c3b315df85a Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Thu, 6 Sep 2012 15:27:23 -0700 Subject: [PATCH 020/179] Changes needed for release plugin --- .gitignore | 4 ++-- gradle/convention.gradle | 6 +++++- gradle/maven.gradle | 5 ----- gradle/release.gradle | 12 +++++++----- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 313af3cb82..79ab4710fa 100644 --- a/.gitignore +++ b/.gitignore @@ -44,8 +44,8 @@ Thumbs.db */target /build */build -# -# # IntelliJ specific files/directories + +# IntelliJ specific files/directories out .idea *.ipr diff --git a/gradle/convention.gradle b/gradle/convention.gradle index 65da4e30df..ce2701a8c5 100644 --- a/gradle/convention.gradle +++ b/gradle/convention.gradle @@ -10,7 +10,7 @@ subprojects { project -> sourceCompatibility = 1.6 // GRADLE-2087 workaround, perform after java plugin - status = version.contains('-SNAPSHOT')?'snapshot':'release' + status = rootProject.status task sourcesJar(type: Jar, dependsOn:classes) { classifier = 'sources' @@ -22,6 +22,10 @@ subprojects { project -> from javadoc.destinationDir } + // Ensure output is on a new line + javadoc.doFirst { println "" } + + artifacts { archives sourcesJar archives javadocJar diff --git a/gradle/maven.gradle b/gradle/maven.gradle index 29f5d405d2..581896b151 100644 --- a/gradle/maven.gradle +++ b/gradle/maven.gradle @@ -29,11 +29,6 @@ subprojects { // Closure to configure all the POM with extra info, common to all projects pom.project { - parent { - groupId 'org.sonatype.oss' - artifactId 'oss-parent' - version '7' - } licenses { license { name 'The Apache Software License, Version 2.0' diff --git a/gradle/release.gradle b/gradle/release.gradle index 8ed0305107..a7f9913d08 100644 --- a/gradle/release.gradle +++ b/gradle/release.gradle @@ -1,4 +1,3 @@ - apply plugin: 'release' // Ignore release plugin's task because it calls out via GradleBuild. This is a good place to put an email to send out @@ -25,6 +24,8 @@ task uploadArtifactory(type: GradleBuild) { startParameter.getExcludedTaskNames().add('check') tasks = [ 'build', 'artifactoryPublish' ] } +task releaseArtifactory(dependsOn: [checkSnapshotDependencies, uploadArtifactory]) + task buildWithArtifactory(type: GradleBuild) { startParameter = project.gradle.startParameter.newInstance() @@ -34,11 +35,11 @@ task buildWithArtifactory(type: GradleBuild) { } // Ensure upload happens before taggging but after all pre-checks -uploadArtifactory.dependsOn checkSnapshotDependencies -createReleaseTag.dependsOn uploadArtifactory +releaseArtifactory.dependsOn checkSnapshotDependencies +createReleaseTag.dependsOn releaseArtifactory gradle.taskGraph.whenReady { taskGraph -> - if ( taskGraph.hasTask(uploadArtifactory) && !taskGraph.hasTask(':release') ) { - throw new GradleException('"release" task has to be run before uploading to Artifactory') + if ( taskGraph.hasTask(uploadArtifactory) && rootProject.status == 'release' && !taskGraph.hasTask(':release') ) { + throw new GradleException('"release" task has to be run before uploading a release to Artifactory') } } subprojects.each { project -> @@ -61,5 +62,6 @@ release { failOnPublishNeeded=true failOnUnversionedFiles=true failOnUpdateNeeded=true + includeProjectNameInTag=true requireBranch = null } From a8674db7d92f5a6669b8f373205cfef52b995ba9 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Mon, 24 Sep 2012 14:53:11 -0700 Subject: [PATCH 021/179] Stop relying on maven convention on project --- build.gradle | 16 ---------------- gradle/maven.gradle | 10 ++++++++++ 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/build.gradle b/build.gradle index 65b498ec84..7f8fb72deb 100644 --- a/build.gradle +++ b/build.gradle @@ -17,22 +17,6 @@ apply from: file('gradle/license.gradle') apply from: file('gradle/release.gradle') subprojects { - // Closure to configure all the POM with extra info, common to all projects - pom { - project { - url "https://github.com/Netflix/${rootProject.githubProjectName}" - scm { - connection "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" - url "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" - developerConnection "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" - } - issueManagement { - system 'github' - url "https://github.com/Netflix/${rootProject.githubProjectName}/issues" - } - } - } - group = "com.netflix.${githubProjectName}" // TEMPLATE: Set to organization of project dependencies { diff --git a/gradle/maven.gradle b/gradle/maven.gradle index 581896b151..1b1e818fd2 100644 --- a/gradle/maven.gradle +++ b/gradle/maven.gradle @@ -36,6 +36,16 @@ subprojects { distribution 'repo' } } + url "https://github.com/Netflix/${rootProject.githubProjectName}" + scm { + connection "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" + url "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" + developerConnection "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" + } + issueManagement { + system 'github' + url "https://github.com/Netflix/${rootProject.githubProjectName}/issues" + } } } } From 6d4a854dca49cbfd989b468efc4e7bce89796e08 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Thu, 4 Oct 2012 17:47:56 -0700 Subject: [PATCH 022/179] Filling in more pom fields for Sonatype --- gradle/maven.gradle | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/gradle/maven.gradle b/gradle/maven.gradle index 1b1e818fd2..a3a3d44240 100644 --- a/gradle/maven.gradle +++ b/gradle/maven.gradle @@ -29,6 +29,15 @@ subprojects { // Closure to configure all the POM with extra info, common to all projects pom.project { + name "${project.name}" + description "${project.name} developed by Netflix" + developers { + developer { + id 'netflixgithub' + name 'Netflix Open Source Development' + email 'talent@netflix.com' + } + } licenses { license { name 'The Apache Software License, Version 2.0' From 61bd2b059be16052e2372e792ec8698af9589d79 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Wed, 10 Oct 2012 20:34:02 -0700 Subject: [PATCH 023/179] Add local publishing --- .gitignore | 1 + gradle/release.gradle | 26 ++++++++++++-------------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 79ab4710fa..c82b5347c5 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ Thumbs.db # Editor Files # ################ *~ +*.swp # Gradle Files # ################ diff --git a/gradle/release.gradle b/gradle/release.gradle index a7f9913d08..cd135643bd 100644 --- a/gradle/release.gradle +++ b/gradle/release.gradle @@ -17,23 +17,21 @@ unSnapshotVersion.dependsOn checkUpdateNeeded checkUpdateNeeded.dependsOn checkCommitNeeded checkCommitNeeded.dependsOn initScmPlugin -// Call out to compile against internal repository -task uploadArtifactory(type: GradleBuild) { - startParameter = project.gradle.startParameter.newInstance() - startParameter.addInitScript( file('gradle/netflix-oss.gradle') ) - startParameter.getExcludedTaskNames().add('check') - tasks = [ 'build', 'artifactoryPublish' ] +[ + uploadIvyLocal: 'uploadLocal', + uploadArtifactory: 'artifactoryPublish', // Call out to compile against internal repository + buildWithArtifactory: 'build' // Build against internal repository +].each { key, value -> + // Call out to compile against internal repository + task "${key}"(type: GradleBuild) { + startParameter = project.gradle.startParameter.newInstance() + startParameter.addInitScript( file('gradle/netflix-oss.gradle') ) + startParameter.getExcludedTaskNames().add('check') + tasks = [ 'build', value ] + } } task releaseArtifactory(dependsOn: [checkSnapshotDependencies, uploadArtifactory]) - -task buildWithArtifactory(type: GradleBuild) { - startParameter = project.gradle.startParameter.newInstance() - startParameter.addInitScript( file('gradle/netflix-oss.gradle') ) - startParameter.getExcludedTaskNames().add('check') - tasks = [ 'build' ] -} - // Ensure upload happens before taggging but after all pre-checks releaseArtifactory.dependsOn checkSnapshotDependencies createReleaseTag.dependsOn releaseArtifactory From 05c4d0660d4f5317e430f0dc7f0c1487ac4f51e2 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Mon, 22 Oct 2012 20:49:26 -0700 Subject: [PATCH 024/179] Putting javadoc and sources into proper confs and setting types --- gradle/convention.gradle | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/gradle/convention.gradle b/gradle/convention.gradle index ce2701a8c5..f16047e2f4 100644 --- a/gradle/convention.gradle +++ b/gradle/convention.gradle @@ -13,23 +13,30 @@ subprojects { project -> status = rootProject.status task sourcesJar(type: Jar, dependsOn:classes) { - classifier = 'sources' from sourceSets.main.allSource } task javadocJar(type: Jar, dependsOn:javadoc) { - classifier = 'javadoc' from javadoc.destinationDir } - // Ensure output is on a new line - javadoc.doFirst { println "" } - + configurations.add('sources') + configurations.add('javadoc') artifacts { - archives sourcesJar - archives javadocJar + sources(sourcesJar) { + type 'source' + classifier 'sources' + } + javadoc(javadocJar) { + type 'javadoc' + classifier 'javadoc' + } } + + // Ensure output is on a new line + javadoc.doFirst { println "" } + } task aggregateJavadoc(type: Javadoc) { From 1cbb4d6dbe34998f4caa2cbe52c59abc8e0e4886 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Tue, 23 Oct 2012 16:05:35 -0700 Subject: [PATCH 025/179] Fixing issue when publishing source/javadoc to maven central --- gradle/convention.gradle | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/gradle/convention.gradle b/gradle/convention.gradle index f16047e2f4..8e71812ecc 100644 --- a/gradle/convention.gradle +++ b/gradle/convention.gradle @@ -14,23 +14,40 @@ subprojects { project -> task sourcesJar(type: Jar, dependsOn:classes) { from sourceSets.main.allSource + classifier 'sources' + extension 'jar' } task javadocJar(type: Jar, dependsOn:javadoc) { from javadoc.destinationDir + classifier 'javadoc' + extension 'jar' } configurations.add('sources') configurations.add('javadoc') + configurations.archives { + extendsFrom configurations.sources + extendsFrom configurations.javadoc + } + + // When outputing to an Ivy repo, we want to use the proper type field + gradle.taskGraph.whenReady { + def isNotMaven = !it.hasTask(project.uploadMavenCentral) + if (isNotMaven) { + def artifacts = project.configurations.sources.artifacts + def sourceArtifact = artifacts.iterator().next() + sourceArtifact.type = 'sources' + } + } artifacts { sources(sourcesJar) { - type 'source' - classifier 'sources' + // Weird Gradle quirk where type will be used for the extension, but only for sources + type 'jar' } javadoc(javadocJar) { type 'javadoc' - classifier 'javadoc' } } From 66ff83f789785e9351cec4fb54b6f6ffa0eb9874 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Fri, 16 Nov 2012 14:46:32 -0800 Subject: [PATCH 026/179] Using a better github location --- gradle/buildscript.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gradle/buildscript.gradle b/gradle/buildscript.gradle index c63c13006f..4d6a29aabe 100644 --- a/gradle/buildscript.gradle +++ b/gradle/buildscript.gradle @@ -1,8 +1,9 @@ // Executed in context of buildscript repositories { + // Repo in addition to maven central maven { name 'build-repo' - url 'https://github.com/Netflix-Skunkworks/build-repo/raw/master/releases/' + url 'https://raw.github.com/Netflix-Skunkworks/build-repo/master/releases/' // gradle-release/gradle-release/1.0-SNAPSHOT/gradle-release-1.0-SNAPSHOT.jar } } dependencies { From 36e5b8f5d64e08147ea98ae17e15a21d716b8b10 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Fri, 14 Dec 2012 11:09:19 -0800 Subject: [PATCH 027/179] Adding provided scope Conflicts: gradle/convention.gradle --- gradle/convention.gradle | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/gradle/convention.gradle b/gradle/convention.gradle index 8e71812ecc..6122c8e878 100644 --- a/gradle/convention.gradle +++ b/gradle/convention.gradle @@ -54,6 +54,20 @@ subprojects { project -> // Ensure output is on a new line javadoc.doFirst { println "" } + configurations { + provided { + description = 'much like compile, but indicates you expect the JDK or a container to provide it. It is only available on the compilation classpath, and is not transitive.' + transitive = false + visible = false + } + } + + project.sourceSets { + main.compileClasspath += project.configurations.provided + main.runtimeClasspath -= project.configurations.provided + test.compileClasspath += project.configurations.provided + test.runtimeClasspath += project.configurations.provided + } } task aggregateJavadoc(type: Javadoc) { From b7847eb946161b85609621f0392eb33e32b5cf42 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Tue, 1 Jan 2013 21:12:24 -0800 Subject: [PATCH 028/179] Fixing transitive-ness of provided --- gradle/convention.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/convention.gradle b/gradle/convention.gradle index 6122c8e878..8b877071d9 100644 --- a/gradle/convention.gradle +++ b/gradle/convention.gradle @@ -57,8 +57,8 @@ subprojects { project -> configurations { provided { description = 'much like compile, but indicates you expect the JDK or a container to provide it. It is only available on the compilation classpath, and is not transitive.' - transitive = false - visible = false + transitive = true + visible = true } } From f5a10c63c56b2cc5c07c7a602be6cd965a8415d3 Mon Sep 17 00:00:00 2001 From: Greg Orzell Date: Thu, 7 Feb 2013 18:35:14 -0800 Subject: [PATCH 029/179] Update codequality/checkstyle.xml Removing as it appears to no longer be available in the latest version of checkstyle that comes with gradle 1.4 --- codequality/checkstyle.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/codequality/checkstyle.xml b/codequality/checkstyle.xml index 481d2829fd..47c01a2ea1 100644 --- a/codequality/checkstyle.xml +++ b/codequality/checkstyle.xml @@ -128,7 +128,6 @@ - From 6f9e3a024fd9213446079e6160ef872849e91c53 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Tue, 5 Mar 2013 10:20:24 -0800 Subject: [PATCH 030/179] Upgrading to Gradle 1.4 --- gradle/convention.gradle | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 45502 -> 46735 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- gradlew | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/gradle/convention.gradle b/gradle/convention.gradle index 8b877071d9..4f07d1a64b 100644 --- a/gradle/convention.gradle +++ b/gradle/convention.gradle @@ -79,5 +79,5 @@ task aggregateJavadoc(type: Javadoc) { // Generate wrapper, which is distributed as part of source to alleviate the need of installing gradle task createWrapper(type: Wrapper) { - gradleVersion = '1.1' + gradleVersion = '1.4' } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f1e239c8466c730b569575c494a89e6bb4c416a..42d9b0e9c5872910311a1d035995ab8ec466e7ac 100644 GIT binary patch delta 9381 zcmZ8n1zeO%*GE#3ZkCc}=?)2{rMp8qB%~Xb5|D-^r39All192aMY=&yIwigZyzl+I z&oaLm{(EBP%$f7d%-NPT*r=bdXo@m$@W?PQPoKghQwxa1pi!gV*ZXWjqk=FnFsd=H z**T8}nR=y$9f8AVMp7Q&$lcv1MHfR@VYt=^Pkt;T){4j)*P~$n!iD#P< z(!o}4vy3Ksl#K88E|L5f4mtr=O*u6|!Vmoy`sa}Vk(L$I3+qCv z>2_OZq$>fkBNpM{O*w7KYEE7c2+S8CtsVLS1+So+SPLbLB&Q*#ROo2#%^SFL#<=Bi zlIthKb%0qm#V>Yb=uW}Gh@@D$`;zlhT?tq;W(%QMdMjkx!UZwq z3B_bV@T(cRzKWtKm{HwQa`BT;V$v5o*A+|mNr|-sNv8>pIJAkUPn^qAL5{M;V{grb zLH3;|CF7LZS!pr}`Z#!B6fl0v%A;s$gF6i1@GWa3o|W5wu!iMZ?^&l8=SsK>y5Q%x z4@Ka&B+7f^-Sz=HDI>SPIfE{dES{1zz-rAwp|87J`UML`LJ_l!2R4A9gX7?lVvHPp?~@L*u*?*)5v5`vK2 zh&Bj)?lcekwyM^8<*m)N0;v)pGQKYE~zP}|?P ztID~2O8@Q<5uvugA~yAX8xd}klsJbxgIiUodi}TX+TJl8(_?Nv??!)Dy1IoZ*TSc( zdi0AC=e=W|rpH3FO-oVe7xofeBy}poz!PR%vd&=Db++s*U-Y6nqAFyetYr`>?woyD zXjR=vR8&6)+40&MvJiQ2MqJAIzz`Bu_V1gEIFl30SEQ;3gat>IvtH9tDJFM(uY}50 zpS!(j!@s$R6}UEi)j|=`SyI-2YsEEryNleCX+)5BbCJ9Eb)%=&^p3wU;;z1rmA?;hXNN1?0a8h`8@O`#4l<%zWu*sSrHev*xRd6#=lpxlTxhC9fyAUnue z@zp0zDm0Em=0kS)dbIIjl{XAcM_@;qcabKBgu(w5QeA(8!o4wwWHoRCDQ;~6^+7k<6 z)0^J^UE&=?>^1za!_>4l5}Px9X5vb3_N6Ejs^roa*K_7GKd55q?e`_cRA6%o+d@T30hh?2i*F;8b5r_?zhN@{yL~&090YsA|{n~3t1ik-1>ig zcafrcx#A1doej;NG3O38eS?!M<1qR{^P>wH zSRstfkW)7u0;Zp1qmuY92gWb6N23al9Id|8n$PC358KGTV4InJKl zXxSc;&c1h4_#Nv9?@YYb&`Z2{wHFwLb!{f7W$cbF?z2SX)t8mEhZXzk{buXLg^VY* zW~UnS7^Wao4(Tf=-4+$M_l!Hp@6nkykipW5h>6>zd5F0aF3>yom( z+3)*R^(S zac?QRYDaF(_B`o?_ow%%1ILV_6Vl)EDB?LyzO0M^bw6wb2mYqO)T|BJy=K0wNwtdN zdBKMYO3*jUsZH~=Lj9HA_%6&bDBb3WTSpA|)}Co&nxEGunRb4awIW(7V>hGXTQzyJ zvdw(-1h{&paCDV!*C40N%vpNpWLO@3pZJV-Z6jTc)I1GqMJ!bCXM>f~m}6zf?mS<5 zlF&KVG}h64;`qC*uv?(dR9ecp$Uy|@_4Y?i(0H8qi|cLS#!<{EF#|}H<7fU29fP@8 zMV$SYmyxw{uFtj(Q#_G;>8tyRVzbp{%kg ztVnX@X=YkB#t%#h$|?O|8!-Z5B<5BEW|#6`&WFo%Mtj}_D;mDS*IcUC1d;=89%NsU zPyof4O0tx~g=h|qXfrB35J%}~iFh((agd-Rq||6hD9o(giZE52;Ww9Pn$EH2z+D5h z?r<+2G4AlT;8*c)`NSxEcVSNmD90`;e{NMi=Lc}o!ppoGL&PQwwSCQD>no0?6ξ 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 Y<0;A@Kqn6dh6(zqg)WZv-24Cj4~6)YzW@LL delta 8312 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9829a99a5b..da2f8ac95a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Aug 14 16:28:54 PDT 2012 +#Tue Mar 05 10:18:48 PST 2013 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=http\://services.gradle.org/distributions/gradle-1.1-bin.zip +distributionUrl=http\://services.gradle.org/distributions/gradle-1.4-bin.zip diff --git a/gradlew b/gradlew index e61422d06d..91a7e269e1 100755 --- a/gradlew +++ b/gradlew @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash ############################################################################## ## @@ -61,9 +61,9 @@ while [ -h "$PRG" ] ; do fi done SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" +cd "`dirname \"$PRG\"`/" >&- APP_HOME="`pwd -P`" -cd "$SAVED" +cd "$SAVED" >&- CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar From 84339e0a40358919863496d6fd8c6035228b6329 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Tue, 5 Mar 2013 11:30:53 -0700 Subject: [PATCH 031/179] Switching to bintray for dependencies (same as Maven Central) --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 7f8fb72deb..80e2f17fb2 100644 --- a/build.gradle +++ b/build.gradle @@ -2,12 +2,12 @@ ext.githubProjectName = rootProject.name // Change if github project name is not the same as the root project's name buildscript { - repositories { mavenCentral() } + repositories { mavenRepo url: 'http://jcenter.bintray.com' } apply from: file('gradle/buildscript.gradle'), to: buildscript } allprojects { - repositories { mavenCentral() } + repositories { mavenRepo url: 'http://jcenter.bintray.com' } } apply from: file('gradle/convention.gradle') From d87b66e0d0074d42b35e46a5c3664dd57294e16d Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Fri, 22 Mar 2013 13:55:49 -0700 Subject: [PATCH 032/179] Move gradle-release dependency to bintray --- gradle/buildscript.gradle | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/gradle/buildscript.gradle b/gradle/buildscript.gradle index 4d6a29aabe..d3f06ec892 100644 --- a/gradle/buildscript.gradle +++ b/gradle/buildscript.gradle @@ -1,13 +1,10 @@ // Executed in context of buildscript repositories { // Repo in addition to maven central - maven { - name 'build-repo' - url 'https://raw.github.com/Netflix-Skunkworks/build-repo/master/releases/' // gradle-release/gradle-release/1.0-SNAPSHOT/gradle-release-1.0-SNAPSHOT.jar - } + repositories { maven { url 'http://dl.bintray.com/content/netflixoss/external-gradle-plugins/' } } // For gradle-release } dependencies { classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.6.0' classpath 'com.mapvine:gradle-cobertura-plugin:0.1' - classpath 'gradle-release:gradle-release:1.0-SNAPSHOT' + classpath 'gradle-release:gradle-release:1.1' } From c3477877da453c81a3c3c121d7677fce2911551d Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Fri, 22 Mar 2013 17:13:07 -0700 Subject: [PATCH 033/179] Use newer version of license-gradle-plugin that fixes skipExistingHeaders field --- build.gradle | 6 ++++-- gradle/buildscript.gradle | 2 +- gradle/license.gradle | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 80e2f17fb2..582aa14d17 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,10 @@ ext.githubProjectName = rootProject.name // Change if github project name is not the same as the root project's name buildscript { - repositories { mavenRepo url: 'http://jcenter.bintray.com' } + repositories { + mavenLocal() + maven { url 'http://jcenter.bintray.com' } + } apply from: file('gradle/buildscript.gradle'), to: buildscript } @@ -44,4 +47,3 @@ project(':template-server') { compile project(':template-client') } } - diff --git a/gradle/buildscript.gradle b/gradle/buildscript.gradle index d3f06ec892..2cb8e60a16 100644 --- a/gradle/buildscript.gradle +++ b/gradle/buildscript.gradle @@ -4,7 +4,7 @@ repositories { repositories { maven { url 'http://dl.bintray.com/content/netflixoss/external-gradle-plugins/' } } // For gradle-release } dependencies { - classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.6.0' + classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.6.1' classpath 'com.mapvine:gradle-cobertura-plugin:0.1' classpath 'gradle-release:gradle-release:1.1' } diff --git a/gradle/license.gradle b/gradle/license.gradle index 11a51f1137..abd2e2c0e1 100644 --- a/gradle/license.gradle +++ b/gradle/license.gradle @@ -5,5 +5,6 @@ apply plugin: 'license' //nl.javadude.gradle.plugins.license.LicensePlugin license { header rootProject.file('codequality/HEADER') ext.year = Calendar.getInstance().get(Calendar.YEAR) + skipExistingHeaders true } } From 883dd0daf391695a3b26e1d0f5c1bb112022c803 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Tue, 26 Mar 2013 12:20:52 -0700 Subject: [PATCH 034/179] Add sonatype snapshot repository --- gradle/maven.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gradle/maven.gradle b/gradle/maven.gradle index a3a3d44240..3bf788d3e8 100644 --- a/gradle/maven.gradle +++ b/gradle/maven.gradle @@ -24,6 +24,10 @@ subprojects { authentication(userName: rootProject.sonatypeUsername, password: rootProject.sonatypePassword) } + snapshotRepository(url: 'https://oss.sonatype.org/content/repositories/snapshots/') { + authentication(userName: rootProject.sonatypeUsername, password: rootProject.sonatypePassword) + } + // Prevent datastamp from being appending to artifacts during deployment uniqueVersion = false From 63e44e8e73b9fdfe56655cc1fb13180885dedc9e Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Tue, 26 Mar 2013 12:52:41 -0700 Subject: [PATCH 035/179] Using latest features of release plugin --- gradle/release.gradle | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/gradle/release.gradle b/gradle/release.gradle index cd135643bd..669c1db684 100644 --- a/gradle/release.gradle +++ b/gradle/release.gradle @@ -7,12 +7,11 @@ task release(overwrite: true, dependsOn: commitNewVersion) << { commitNewVersion.dependsOn updateVersion updateVersion.dependsOn createReleaseTag createReleaseTag.dependsOn preTagCommit -def buildTasks = tasks.matching { it.name =~ /:build/ } -preTagCommit.dependsOn buildTasks +preTagCommit.dependsOn build +preTagCommit.dependsOn buildWithArtifactory preTagCommit.dependsOn checkSnapshotDependencies -//checkSnapshotDependencies.dependsOn confirmReleaseVersion // Introduced in 1.0, forces readLine -//confirmReleaseVersion.dependsOn unSnapshotVersion -checkSnapshotDependencies.dependsOn unSnapshotVersion // Remove once above is fixed +checkSnapshotDependencies.dependsOn confirmReleaseVersion +confirmReleaseVersion.dependsOn unSnapshotVersion unSnapshotVersion.dependsOn checkUpdateNeeded checkUpdateNeeded.dependsOn checkCommitNeeded checkCommitNeeded.dependsOn initScmPlugin @@ -30,23 +29,18 @@ checkCommitNeeded.dependsOn initScmPlugin tasks = [ 'build', value ] } } -task releaseArtifactory(dependsOn: [checkSnapshotDependencies, uploadArtifactory]) +task releaseArtifactory(dependsOn: [preTagCommit, uploadArtifactory]) // Ensure upload happens before taggging but after all pre-checks -releaseArtifactory.dependsOn checkSnapshotDependencies createReleaseTag.dependsOn releaseArtifactory -gradle.taskGraph.whenReady { taskGraph -> - if ( taskGraph.hasTask(uploadArtifactory) && rootProject.status == 'release' && !taskGraph.hasTask(':release') ) { - throw new GradleException('"release" task has to be run before uploading a release to Artifactory') - } -} + subprojects.each { project -> - project.uploadMavenCentral.dependsOn rootProject.checkSnapshotDependencies + project.uploadMavenCentral.dependsOn rootProject.preTagCommit rootProject.createReleaseTag.dependsOn project.uploadMavenCentral gradle.taskGraph.whenReady { taskGraph -> - if ( taskGraph.hasTask(project.uploadMavenCentral) && !taskGraph.hasTask(':release') ) { - throw new GradleException('"release" task has to be run before uploading to Maven Central') + if ( rootProject.status == 'release' && !taskGraph.hasTask(':release') ) { + throw new GradleException('"release" task has to be run before uploading a release') } } } @@ -55,11 +49,12 @@ subprojects.each { project -> ext.'gradle.release.useAutomaticVersion' = "true" release { - // http://tellurianring.com/wiki/gradle/release failOnCommitNeeded=true failOnPublishNeeded=true + failOnSnapshotDependencies=true failOnUnversionedFiles=true failOnUpdateNeeded=true includeProjectNameInTag=true + revertOnFail=true requireBranch = null } From 8caf8ec93b3617749db90ceac64e06116567f072 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Tue, 26 Mar 2013 12:52:41 -0700 Subject: [PATCH 036/179] Upgrading release process --- build.gradle | 6 ++-- gradle/buildscript.gradle | 2 +- gradle/check.gradle | 41 +++++++++++----------- gradle/convention.gradle | 13 ++----- gradle/maven.gradle | 19 ++++++----- gradle/release.gradle | 71 ++++++++++++++++++--------------------- 6 files changed, 72 insertions(+), 80 deletions(-) diff --git a/build.gradle b/build.gradle index 582aa14d17..5e6c63bb99 100644 --- a/build.gradle +++ b/build.gradle @@ -4,13 +4,15 @@ ext.githubProjectName = rootProject.name // Change if github project name is not buildscript { repositories { mavenLocal() - maven { url 'http://jcenter.bintray.com' } + mavenCentral() // maven { url 'http://jcenter.bintray.com' } } apply from: file('gradle/buildscript.gradle'), to: buildscript } allprojects { - repositories { mavenRepo url: 'http://jcenter.bintray.com' } + repositories { + mavenCentral() // maven { url: 'http://jcenter.bintray.com' } + } } apply from: file('gradle/convention.gradle') diff --git a/gradle/buildscript.gradle b/gradle/buildscript.gradle index 2cb8e60a16..b6fb61e167 100644 --- a/gradle/buildscript.gradle +++ b/gradle/buildscript.gradle @@ -6,5 +6,5 @@ repositories { dependencies { classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.6.1' classpath 'com.mapvine:gradle-cobertura-plugin:0.1' - classpath 'gradle-release:gradle-release:1.1' + classpath 'gradle-release:gradle-release:1.1.4' } diff --git a/gradle/check.gradle b/gradle/check.gradle index 7617f17b35..a3e4b4e7f5 100644 --- a/gradle/check.gradle +++ b/gradle/check.gradle @@ -1,25 +1,26 @@ subprojects { - // Checkstyle - apply plugin: 'checkstyle' - tasks.withType(Checkstyle) { ignoreFailures = true } - checkstyle { - ignoreFailures = true // Waiting on GRADLE-2163 - configFile = rootProject.file('codequality/checkstyle.xml') - } +// Checkstyle +apply plugin: 'checkstyle' +checkstyle { + ignoreFailures = true + configFile = rootProject.file('codequality/checkstyle.xml') +} - // FindBugs - apply plugin: 'findbugs' - //tasks.withType(Findbugs) { reports.html.enabled true } +// FindBugs +apply plugin: 'findbugs' +findbugs { + ignoreFailures = true +} - // PMD - apply plugin: 'pmd' - //tasks.withType(Pmd) { reports.html.enabled true } +// PMD +apply plugin: 'pmd' +//tasks.withType(Pmd) { reports.html.enabled true } - apply plugin: 'cobertura' - cobertura { - sourceDirs = sourceSets.main.java.srcDirs - format = 'html' - includes = ['**/*.java', '**/*.groovy'] - excludes = [] - } +apply plugin: 'cobertura' +cobertura { + sourceDirs = sourceSets.main.java.srcDirs + format = 'html' + includes = ['**/*.java', '**/*.groovy'] + excludes = [] +} } diff --git a/gradle/convention.gradle b/gradle/convention.gradle index 4f07d1a64b..36650a2f98 100644 --- a/gradle/convention.gradle +++ b/gradle/convention.gradle @@ -1,15 +1,11 @@ - -// For Artifactory -rootProject.status = version.contains('-SNAPSHOT')?'snapshot':'release' +status = version.contains('SNAPSHOT')?'snapshot':status subprojects { project -> apply plugin: 'java' // Plugin as major conventions - version = rootProject.version - sourceCompatibility = 1.6 - // GRADLE-2087 workaround, perform after java plugin + // Restore status after Java plugin status = rootProject.status task sourcesJar(type: Jar, dependsOn:classes) { @@ -51,9 +47,6 @@ subprojects { project -> } } - // Ensure output is on a new line - javadoc.doFirst { println "" } - configurations { provided { description = 'much like compile, but indicates you expect the JDK or a container to provide it. It is only available on the compilation classpath, and is not transitive.' @@ -79,5 +72,5 @@ task aggregateJavadoc(type: Javadoc) { // Generate wrapper, which is distributed as part of source to alleviate the need of installing gradle task createWrapper(type: Wrapper) { - gradleVersion = '1.4' + gradleVersion = '1.5' } diff --git a/gradle/maven.gradle b/gradle/maven.gradle index 3bf788d3e8..b924757625 100644 --- a/gradle/maven.gradle +++ b/gradle/maven.gradle @@ -8,14 +8,16 @@ subprojects { sign configurations.archives } - /** - * Publishing to Maven Central example provided from http://jedicoder.blogspot.com/2011/11/automated-gradle-project-deployment-to.html - */ - task uploadMavenCentral(type:Upload, dependsOn: signArchives) { - configuration = configurations.archives - doFirst { - repositories.mavenDeployer { - beforeDeployment { org.gradle.api.artifacts.maven.MavenDeployment deployment -> signing.signPom(deployment) } +/** + * Publishing to Maven Central example provided from http://jedicoder.blogspot.com/2011/11/automated-gradle-project-deployment-to.html + * artifactory will execute uploadArchives to force generation of ivy.xml, and we don't want that to trigger an upload to maven + * central, so using custom upload task. + */ +task uploadMavenCentral(type:Upload, dependsOn: signArchives) { + configuration = configurations.archives + onlyIf { ['release', 'snapshot'].contains(project.status) } + repositories.mavenDeployer { + beforeDeployment { signing.signPom(it) } // To test deployment locally, use the following instead of oss.sonatype.org //repository(url: "file://localhost/${rootProject.rootDir}/repo") @@ -62,5 +64,4 @@ subprojects { } } } - } } diff --git a/gradle/release.gradle b/gradle/release.gradle index 669c1db684..23a1a68227 100644 --- a/gradle/release.gradle +++ b/gradle/release.gradle @@ -1,47 +1,49 @@ apply plugin: 'release' -// Ignore release plugin's task because it calls out via GradleBuild. This is a good place to put an email to send out -task release(overwrite: true, dependsOn: commitNewVersion) << { - // This is a good place to put an email to send out -} -commitNewVersion.dependsOn updateVersion -updateVersion.dependsOn createReleaseTag -createReleaseTag.dependsOn preTagCommit -preTagCommit.dependsOn build -preTagCommit.dependsOn buildWithArtifactory -preTagCommit.dependsOn checkSnapshotDependencies -checkSnapshotDependencies.dependsOn confirmReleaseVersion -confirmReleaseVersion.dependsOn unSnapshotVersion -unSnapshotVersion.dependsOn checkUpdateNeeded -checkUpdateNeeded.dependsOn checkCommitNeeded -checkCommitNeeded.dependsOn initScmPlugin - -[ - uploadIvyLocal: 'uploadLocal', - uploadArtifactory: 'artifactoryPublish', // Call out to compile against internal repository - buildWithArtifactory: 'build' // Build against internal repository -].each { key, value -> +[ uploadIvyLocal: 'uploadLocal', uploadArtifactory: 'artifactoryPublish', buildWithArtifactory: 'build' ].each { key, value -> // Call out to compile against internal repository task "${key}"(type: GradleBuild) { startParameter = project.gradle.startParameter.newInstance() + doFirst { + startParameter.projectProperties = [status: project.status] + } startParameter.addInitScript( file('gradle/netflix-oss.gradle') ) startParameter.getExcludedTaskNames().add('check') tasks = [ 'build', value ] } } -task releaseArtifactory(dependsOn: [preTagCommit, uploadArtifactory]) + +// Marker task for following code to key in on +task releaseCandidate(dependsOn: release) +task forceCandidate { + onlyIf { gradle.taskGraph.hasTask(releaseCandidate) } + doFirst { project.status = 'candidate' } +} +release.dependsOn(forceCandidate) + +task releaseSnapshot(dependsOn: [uploadArtifactory, uploadMavenCentral]) + +// Ensure our versions look like the project status before publishing +task verifyStatus << { + def hasSnapshot = version.contains('-SNAPSHOT') + if (project.status == 'snapshot' && !hasSnapshot) { + throw new GradleException("Version (${version}) needs -SNAPSHOT if publishing snapshot") + } +} +uploadArtifactory.dependsOn(verifyStatus) +uploadMavenCentral.dependsOn(verifyStatus) // Ensure upload happens before taggging but after all pre-checks -createReleaseTag.dependsOn releaseArtifactory +createReleaseTag.dependsOn([uploadArtifactory, uploadMavenCentral]) -subprojects.each { project -> - project.uploadMavenCentral.dependsOn rootProject.preTagCommit - rootProject.createReleaseTag.dependsOn project.uploadMavenCentral +gradle.taskGraph.whenReady { taskGraph -> + def hasRelease = taskGraph.hasTask('commitNewVersion') + def indexOf = { return taskGraph.allTasks.indexOf(it) } - gradle.taskGraph.whenReady { taskGraph -> - if ( rootProject.status == 'release' && !taskGraph.hasTask(':release') ) { - throw new GradleException('"release" task has to be run before uploading a release') - } + if (hasRelease) { + assert indexOf(build) < indexOf(unSnapshotVersion), 'build target has to be after unSnapshotVersion' + assert indexOf(uploadMavenCentral) < indexOf(preTagCommit), 'preTagCommit has to be after uploadMavenCentral' + assert indexOf(uploadArtifactory) < indexOf(preTagCommit), 'preTagCommit has to be after uploadArtifactory' } } @@ -49,12 +51,5 @@ subprojects.each { project -> ext.'gradle.release.useAutomaticVersion' = "true" release { - failOnCommitNeeded=true - failOnPublishNeeded=true - failOnSnapshotDependencies=true - failOnUnversionedFiles=true - failOnUpdateNeeded=true - includeProjectNameInTag=true - revertOnFail=true - requireBranch = null + git.requireBranch = null } From 498c25feaa03a9a3a09e633814c192ddc770ced4 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Tue, 2 Apr 2013 11:48:33 -0700 Subject: [PATCH 037/179] Matching wrapper to 1.5 --- gradle/wrapper/gradle-wrapper.jar | Bin 46735 -> 46742 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 42d9b0e9c5872910311a1d035995ab8ec466e7ac..faa569a9a0eedc9ff37450fed24a7efd77a86729 100644 GIT binary patch delta 1766 zcmZWp3ou+)7(RPEYca_URV;%FkcmMtF!lyq^dKqZ1P8>#luLktBR1bVmC9$S*?ScWxu&(q6qrvyrAK z-KKOHmCs=SU=3hNn@z5d?92DQwl|-tG~qk-ui1V z_S|_fJ#?fp=h0|Q!ht9u+p4cT$7z1$qdcqJxrTDy4m@Hkw#0l93fel&2GHo1=EMs71D(WE{8`{%OI7l~} zaF8}E6@CU5)ZRV6JL#`R`=(|BZ~!LC9HCbGitc)fmX?sE`p^=E=~FZlli5Y5G0U1Y z$LdR$)@f$`@&aw#E@m6h6_hdQeD}(q(o17id0WcWfh_D(T02QIiaI&%(_df4G@^=OkW)j}q^18_?1{qinAuI3Oma8RNVVa(C^V(iuUWBf}a!+5mm z9L9krDMsVwXpD;HSd7EX5g7R`VKm=DuYc8Y0)JbzMqzxn^$f=8*6{z#x1cS=j%q-E zGSP@`J&pM+sBCkg`iBE;WWZ)90O!(^Mak)jinOz$GifOzc^HGRWYQO&k}CTB&xAz+ zfD$$Up8x6~Y|M9)z(9$Y+s*)S{8^gpVOxXvQ+g)(U zdkQ#}5alT_0N@S1un_3k5st(1I!MAd5{22qfsTEcnn%=F6jrk96ktlyNfIrm5#be@=Ql6ZUxYmh~|VA~e`1)}d?Mzkwr=&b2RbtExV zjE?G6xK`}=zvF_R4O$JN675lm?s_GT!5cb1EHRHHrpmD@uYyl)45lSLByp<>(YA2B zmdD#M2=E444ync^=2eq$vxkl{=_QFPwXm+%&wWQkP>n7$mx*fWX8|DAt0k@u>r~WP yygU}AZ_WP0=l)v(?C=Ue+q5=U5GEfi@l|V}g66-pVReDBPppl1_?S3VTL zjf@crwWdTqVTFwFXD<@+lmLJy=;6X5^VLvca0^qiS^Z3Ggc|c$*vKwDr*aD zjJul9M>Cs?O|a+imXB!TSqO{3p2@Ip*v6DpdgW@<82D9W_@3FmpW>$Yy>UJ*u|7T; zUj|pe`rubfmJ4%dM$=LGyz1vsY5J2_^6qmL@x!AP(M|G!8`9pP_++oQS}Z{sulc3h zb)drk1<)bN@ypX4x!q2d#RaJwwsl|1+1Fol(l!Nd%4sdz?s&Gy=44~=7gt>ahI=P2 zP8SSy6+}mJ2KqKTw|#rhB0sB^PW(M2A?bxT&frroRm*fGz1dr2f z)`wm%lIq@%oS6T-@^zAP_FX|rV$Bt^fa@2=z0_7M-hsa}`5w2Wm7_Lxm%=sS&w>{1 zXBLP1F7GWIeOxvA8CS)YNJ?j_f+auBR?Vljvn0Jj?VlEh4ZA|J*DpFw^jNH19_kDl z^F5gewSjy;CS|}};dAUrig`_7LoE7>Hg?8K8Q3!@9%5tTO3DTtqWlOZ|$?UD$RK{@$UTyba4Wdi>H^zJ06oen_Mt4JITMjn2r8z)&OkRBX)~*f+i9N_&`kyikeB{ za>~*X2TrC~>O@D5R2_PX1ancoP`zCD({#CJo87T+)cncYa|QrFQvlrb$m|1{yidIb z6X$;E4^g026lezn^@zJOF;T5{$5S6xlZ+kkjoKf-@iZjk5EN>n@S99S62g345sx%H z?B=wP1mhrfhaEJ7vR1ms{m4CZT*sc0v*5-Poc~>x=uY~nQq*q+3jja8$_7f7f^DF= z*4mM}*@bJnT{Ng)XEOkT^vKs?sFsh*Ii)3uZYi8obK!FL0X-$nGNhqI3!#5M_s%bA zaG+-^7srcQN#bT1a(hraho=|TSHpXbCRd2YtiMn@%IWxWuE)Du2tkmn5RH5u32)V_ zwHSJ}MPjW(Z6vX`dZ`39tgN=ut;i#Eq?GH9rsVc|3d>)H2$XpjdK~o>LSbsVuVI&h Ga{dRU8)+K= diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index da2f8ac95a..061b536b4b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Mar 05 10:18:48 PST 2013 +#Tue Apr 02 11:45:56 PDT 2013 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=http\://services.gradle.org/distributions/gradle-1.4-bin.zip +distributionUrl=http\://services.gradle.org/distributions/gradle-1.5-bin.zip From 832eb537e9df36a2e53a90e1f34e2008a36f3f17 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Tue, 2 Apr 2013 17:30:25 -0700 Subject: [PATCH 038/179] Automatically aggregate and publish docs (java,groovy,scala) --- gradle/buildscript.gradle | 1 + gradle/convention.gradle | 33 ++++++++++++++++++++++++++++----- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/gradle/buildscript.gradle b/gradle/buildscript.gradle index b6fb61e167..0f6555176f 100644 --- a/gradle/buildscript.gradle +++ b/gradle/buildscript.gradle @@ -7,4 +7,5 @@ dependencies { classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.6.1' classpath 'com.mapvine:gradle-cobertura-plugin:0.1' classpath 'gradle-release:gradle-release:1.1.4' + classpath 'org.ajoberstar:gradle-git:0.5.0' } diff --git a/gradle/convention.gradle b/gradle/convention.gradle index 36650a2f98..1056bd5de9 100644 --- a/gradle/convention.gradle +++ b/gradle/convention.gradle @@ -63,11 +63,34 @@ subprojects { project -> } } -task aggregateJavadoc(type: Javadoc) { - description = 'Aggregate all subproject docs into a single docs directory' - source subprojects.collect {project -> project.sourceSets.main.allJava } - classpath = files(subprojects.collect {project -> project.sourceSets.main.compileClasspath}) - destinationDir = new File(projectDir, 'doc') +apply plugin: 'github-pages' // Used to create publishGhPages task + +def docTasks = [:] +[Javadoc,ScalaDoc,Groovydoc].each{ Class docClass -> + def allSources = allprojects.tasks*.withType(docClass).flatten()*.source + if (allSources) { + def shortName = docClass.simpleName.toLowerCase() + def docTask = task "aggregate${shortName.capitalize()}"(type: docClass, description: "Aggregate subproject ${shortName}s") { + source = allSources + doFirst { + def classpaths = allprojects.findAll { it.plugins.hasPlugin(JavaPlugin) }.collect { it.sourceSets.main.compileClasspath } + classpath = files(classpaths) + } + } + docTasks[shortName] = docTask + processGhPages.dependsOn(docTask) + } +} + +githubPages { + repoUri = "git@github.com:quidryan/${rootProject.githubProjectName}.git" + pages { + docTasks.each { shortName, docTask -> + from(docTask.outputs.files) { + into "docs/${shortName}" + } + } + } } // Generate wrapper, which is distributed as part of source to alleviate the need of installing gradle From 9de561434f303b8020269984d7ef313428414715 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Tue, 2 Apr 2013 17:39:50 -0700 Subject: [PATCH 039/179] Make uploadMavenCentral task, that encompasses other tasks --- gradle/release.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/gradle/release.gradle b/gradle/release.gradle index 23a1a68227..9daa84736f 100644 --- a/gradle/release.gradle +++ b/gradle/release.gradle @@ -21,6 +21,7 @@ task forceCandidate { } release.dependsOn(forceCandidate) +task uploadMavenCentral(dependsOn: subprojects.tasks.uploadMavenCentral) task releaseSnapshot(dependsOn: [uploadArtifactory, uploadMavenCentral]) // Ensure our versions look like the project status before publishing From d0e42e3dfb45c8d792c6cced1c943c89b1d861dc Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Mon, 8 Apr 2013 10:04:45 -0700 Subject: [PATCH 040/179] Handle unavailable sonatype properties --- gradle/maven.gradle | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/gradle/maven.gradle b/gradle/maven.gradle index b924757625..817846d77f 100644 --- a/gradle/maven.gradle +++ b/gradle/maven.gradle @@ -22,12 +22,15 @@ task uploadMavenCentral(type:Upload, dependsOn: signArchives) { // To test deployment locally, use the following instead of oss.sonatype.org //repository(url: "file://localhost/${rootProject.rootDir}/repo") + def sonatypeUsername = rootProject.hasProperty('sonatypeUsername')?rootProject.sonatypeUsername:'' + def sonatypePassword = rootProject.hasProperty('sonatypePassword')?rootProject.sonatypePassword:'' + repository(url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2') { - authentication(userName: rootProject.sonatypeUsername, password: rootProject.sonatypePassword) + authentication(userName: sonatypeUsername, password: sonatypePassword) } snapshotRepository(url: 'https://oss.sonatype.org/content/repositories/snapshots/') { - authentication(userName: rootProject.sonatypeUsername, password: rootProject.sonatypePassword) + authentication(userName: sonatypeUsername, password: sonatypePassword) } // Prevent datastamp from being appending to artifacts during deployment From 28c6989099fc538cfd9d1d8d13075901bcec769d Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Fri, 12 Apr 2013 12:29:08 -0700 Subject: [PATCH 041/179] Verify before we can't take it back, use preferredVersion variable --- gradle/buildscript.gradle | 2 +- gradle/convention.gradle | 3 ++- gradle/release.gradle | 11 ++++++++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/gradle/buildscript.gradle b/gradle/buildscript.gradle index 0f6555176f..0b6da7ce84 100644 --- a/gradle/buildscript.gradle +++ b/gradle/buildscript.gradle @@ -6,6 +6,6 @@ repositories { dependencies { classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.6.1' classpath 'com.mapvine:gradle-cobertura-plugin:0.1' - classpath 'gradle-release:gradle-release:1.1.4' + classpath 'gradle-release:gradle-release:1.1.5' classpath 'org.ajoberstar:gradle-git:0.5.0' } diff --git a/gradle/convention.gradle b/gradle/convention.gradle index 1056bd5de9..2720c8b402 100644 --- a/gradle/convention.gradle +++ b/gradle/convention.gradle @@ -1,4 +1,5 @@ -status = version.contains('SNAPSHOT')?'snapshot':status +// GRADLE-2087 workaround, perform after java plugin +status = project.hasProperty('preferredStatus')?project.preferredStatus:(version.contains('SNAPSHOT')?'snapshot':'release') subprojects { project -> apply plugin: 'java' // Plugin as major conventions diff --git a/gradle/release.gradle b/gradle/release.gradle index 9daa84736f..06acd17da8 100644 --- a/gradle/release.gradle +++ b/gradle/release.gradle @@ -19,7 +19,11 @@ task forceCandidate { onlyIf { gradle.taskGraph.hasTask(releaseCandidate) } doFirst { project.status = 'candidate' } } -release.dependsOn(forceCandidate) +task forceRelease { + onlyIf { !gradle.taskGraph.hasTask(releaseCandidate) } + doFirst { project.status = 'release' } +} +release.dependsOn([forceCandidate, forceRelease]) task uploadMavenCentral(dependsOn: subprojects.tasks.uploadMavenCentral) task releaseSnapshot(dependsOn: [uploadArtifactory, uploadMavenCentral]) @@ -34,8 +38,9 @@ task verifyStatus << { uploadArtifactory.dependsOn(verifyStatus) uploadMavenCentral.dependsOn(verifyStatus) -// Ensure upload happens before taggging but after all pre-checks -createReleaseTag.dependsOn([uploadArtifactory, uploadMavenCentral]) +// Ensure upload happens before taggging, hence upload failures will leave repo in a revertable state +preTagCommit.dependsOn([uploadArtifactory, uploadMavenCentral]) + gradle.taskGraph.whenReady { taskGraph -> def hasRelease = taskGraph.hasTask('commitNewVersion') From b0585dae4cfe7108fd8bd80d7c4b890f95e42119 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Fri, 12 Apr 2013 13:27:39 -0700 Subject: [PATCH 042/179] Passing status to Artifactory builds --- gradle/release.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/release.gradle b/gradle/release.gradle index 06acd17da8..7979dc3a18 100644 --- a/gradle/release.gradle +++ b/gradle/release.gradle @@ -5,7 +5,7 @@ apply plugin: 'release' task "${key}"(type: GradleBuild) { startParameter = project.gradle.startParameter.newInstance() doFirst { - startParameter.projectProperties = [status: project.status] + startParameter.projectProperties = [status: project.status, preferredStatus: project.status] } startParameter.addInitScript( file('gradle/netflix-oss.gradle') ) startParameter.getExcludedTaskNames().add('check') From 6df65bf346344a1b29abc51c2f3e866b6a9d2c55 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Thu, 20 Jun 2013 13:19:06 -0700 Subject: [PATCH 043/179] Fixing aggregateJavadoc --- gradle/convention.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gradle/convention.gradle b/gradle/convention.gradle index 2720c8b402..c4658fc33e 100644 --- a/gradle/convention.gradle +++ b/gradle/convention.gradle @@ -73,6 +73,7 @@ def docTasks = [:] def shortName = docClass.simpleName.toLowerCase() def docTask = task "aggregate${shortName.capitalize()}"(type: docClass, description: "Aggregate subproject ${shortName}s") { source = allSources + destinationDir = file("${project.buildDir}/docs/${shortName}") doFirst { def classpaths = allprojects.findAll { it.plugins.hasPlugin(JavaPlugin) }.collect { it.sourceSets.main.compileClasspath } classpath = files(classpaths) @@ -84,7 +85,7 @@ def docTasks = [:] } githubPages { - repoUri = "git@github.com:quidryan/${rootProject.githubProjectName}.git" + repoUri = "git@github.com:Netflix/${rootProject.githubProjectName}.git" pages { docTasks.each { shortName, docTask -> from(docTask.outputs.files) { From 222fc122e2438b30b553f9c0c8b6db80f417ec2a Mon Sep 17 00:00:00 2001 From: adriancole Date: Wed, 26 Jun 2013 18:07:32 -0700 Subject: [PATCH 044/179] initial import --- .gitignore | 8 +- README.md | 102 +++- build.gradle | 30 +- feign-core/src/main/java/feign/Client.java | 114 ++++ feign-core/src/main/java/feign/Contract.java | 120 ++++ feign-core/src/main/java/feign/Feign.java | 148 +++++ .../src/main/java/feign/FeignException.java | 51 ++ .../src/main/java/feign/MethodHandler.java | 127 +++++ .../src/main/java/feign/MethodMetadata.java | 76 +++ .../src/main/java/feign/ReflectiveFeign.java | 247 ++++++++ feign-core/src/main/java/feign/Request.java | 113 ++++ .../src/main/java/feign/RequestTemplate.java | 533 ++++++++++++++++++ feign-core/src/main/java/feign/Response.java | 183 ++++++ .../main/java/feign/RetryableException.java | 48 ++ feign-core/src/main/java/feign/Retryer.java | 66 +++ feign-core/src/main/java/feign/Target.java | 98 ++++ feign-core/src/main/java/feign/Wire.java | 139 +++++ .../main/java/feign/codec/BodyEncoder.java | 41 ++ .../src/main/java/feign/codec/Decoder.java | 84 +++ .../src/main/java/feign/codec/Decoders.java | 121 ++++ .../main/java/feign/codec/ErrorDecoder.java | 130 +++++ .../main/java/feign/codec/FormEncoder.java | 27 + .../src/main/java/feign/codec/SAXDecoder.java | 50 ++ .../java/feign/codec/ToStringDecoder.java | 12 + .../src/test/java/feign/ContractTest.java | 126 +++++ .../test/java/feign/DefaultRetryerTest.java | 59 ++ feign-core/src/test/java/feign/FeignTest.java | 168 ++++++ .../test/java/feign/RequestTemplateTest.java | 72 +++ .../java/feign/TrustingSSLSocketFactory.java | 99 ++++ .../feign/codec/DefaultErrorDecoderTest.java | 38 ++ .../feign/codec/RetryAfterDecoderTest.java | 46 ++ .../java/feign/examples/GitHubExample.java | 93 +++ .../test/java/feign/examples/IAMExample.java | 201 +++++++ gradle.properties | 2 +- settings.gradle | 4 +- 35 files changed, 3549 insertions(+), 27 deletions(-) create mode 100644 feign-core/src/main/java/feign/Client.java create mode 100644 feign-core/src/main/java/feign/Contract.java create mode 100644 feign-core/src/main/java/feign/Feign.java create mode 100644 feign-core/src/main/java/feign/FeignException.java create mode 100644 feign-core/src/main/java/feign/MethodHandler.java create mode 100644 feign-core/src/main/java/feign/MethodMetadata.java create mode 100644 feign-core/src/main/java/feign/ReflectiveFeign.java create mode 100644 feign-core/src/main/java/feign/Request.java create mode 100644 feign-core/src/main/java/feign/RequestTemplate.java create mode 100644 feign-core/src/main/java/feign/Response.java create mode 100644 feign-core/src/main/java/feign/RetryableException.java create mode 100644 feign-core/src/main/java/feign/Retryer.java create mode 100644 feign-core/src/main/java/feign/Target.java create mode 100644 feign-core/src/main/java/feign/Wire.java create mode 100644 feign-core/src/main/java/feign/codec/BodyEncoder.java create mode 100644 feign-core/src/main/java/feign/codec/Decoder.java create mode 100644 feign-core/src/main/java/feign/codec/Decoders.java create mode 100644 feign-core/src/main/java/feign/codec/ErrorDecoder.java create mode 100644 feign-core/src/main/java/feign/codec/FormEncoder.java create mode 100644 feign-core/src/main/java/feign/codec/SAXDecoder.java create mode 100644 feign-core/src/main/java/feign/codec/ToStringDecoder.java create mode 100644 feign-core/src/test/java/feign/ContractTest.java create mode 100644 feign-core/src/test/java/feign/DefaultRetryerTest.java create mode 100644 feign-core/src/test/java/feign/FeignTest.java create mode 100644 feign-core/src/test/java/feign/RequestTemplateTest.java create mode 100644 feign-core/src/test/java/feign/TrustingSSLSocketFactory.java create mode 100644 feign-core/src/test/java/feign/codec/DefaultErrorDecoderTest.java create mode 100644 feign-core/src/test/java/feign/codec/RetryAfterDecoderTest.java create mode 100644 feign-core/src/test/java/feign/examples/GitHubExample.java create mode 100644 feign-core/src/test/java/feign/examples/IAMExample.java diff --git a/.gitignore b/.gitignore index c82b5347c5..5b07c032e3 100644 --- a/.gitignore +++ b/.gitignore @@ -39,12 +39,16 @@ Thumbs.db # Gradle Files # ################ .gradle +local.properties # Build output directies /target -*/target -/build +**/test-output +**/target +**/bin +build */build +.m2 # IntelliJ specific files/directories out diff --git a/README.md b/README.md index ebf660a86f..0bf18dc73a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,100 @@ -feign -===== +# Feign makes writing java http clients easier +Feign is a java to http client binder inspired by [Dagger](https://github.com/square/dagger), [Retrofit](https://github.com/square/retrofit), [jclouds](https://github.com/jclouds/jclouds), and [JAXRS-2.0](https://jax-rs-spec.java.net/nonav/2.0/apidocs/index.html). Feign's first goal was reducing the complexity of binding [Denominator](https://github.com/Netflix/Denominator) uniformly to http apis regardless of [restfulness](http://www.slideshare.net/adrianfcole/99problems). + +### Why Feign and not X? + +You can use tools like Jersey and CXF to write java clients for ReST or SOAP services. You can write your own code on top of http transport libraries like Apache HC. Feign aims to connect your code to http apis with minimal overhead and code. Via customizable decoders and error handling, you should be able to write to any text-based http api. + +### How does Feign work? + +Feign works by processing annotations into a templatized request. Just before sending it off, arguments are applied to these templates in a straightforward fashion. While this limits Feign to only supporting text-based apis, it dramatically simplified system aspects such as replaying requests. It is also stupid easy to unit test your conversions knowing this. + +### Basics + +Usage typically looks like this, an adaptation of the [canonical Retrofit sample](https://github.com/square/retrofit/blob/master/retrofit-samples/github-client/src/main/java/com/example/retrofit/GitHubClient.java). + +```java +interface GitHub { + @GET @Path("/repos/{owner}/{repo}/contributors") List contributors(@PathParam("owner") String owner, @PathParam("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 contributors = github.contributors("netflix", "feign"); + for (Contributor contributor : contributors) { + System.out.println(contributor.login + " (" + contributor.contributions + ")"); + } +} +``` +### Decoders +The last argument to `Feign.create` specifies how to decode the responses. You can plug-in your favorite library, such as gson, or use builtin RegEx Pattern decoders. Here's how the Gson module looks. + +```java +@Module(overrides = true, library = true) +static class GsonModule { + @Provides @Singleton Map decoders() { + return ImmutableMap.of("GitHub", gsonDecoder); + } + + final Decoder gsonDecoder = new Decoder() { + Gson gson = new Gson(); + + @Override public Object decode(String methodKey, Reader reader, TypeToken type) { + return gson.fromJson(reader, type.getType()); + } + }; +} +``` +Feign doesn't offer a built-in json decoder as you can see above it is very few lines of code to wire yours in. If you are a jackson user, you'd probably thank us for not dragging in a dependency you don't use. + +### Multiple Interfaces +Feign can produce multiple api interfaces. These are defined as `Target` (default `HardCodedTarget`), which allow for dynamic discovery and decoration of requests prior to execution. + +For example, the following pattern might decorate each request with the current url and auth token from the identity service. + +```java +CloudDNS cloudDNS = Feign.create().newInstance(new CloudIdentityTarget(user, apiKey)); +``` + +You can find [several examples](https://github.com/Netflix/feign/tree/master/feign-core/src/test/java/feign/examples) in the test tree. Do take time to look at them, as seeing is believing! +### Advanced usage and Dagger +#### Dagger +Feign can be directly wired into Dagger which keeps things at compile time and Android friendly. As opposed to exposing builders for config, Feign intends users to embed their config in Dagger. + +Almost all configuration of Feign is represented as Map bindings, where the key is either the simple name (ex. `GitHub`) or the method (ex. `GitHub#contributors()`) in javadoc link format. For example, the following routes all decoding to gson: +```java +@Provides @Singleton Map decoders() { + return ImmutableMap.of("GitHub", gsonDecoder); +} +``` +#### Wire Logging +You can log the http messages going to and from the target by setting up a `Wire`. Here's the easiest way to do that: +```java +@Module(overrides = true) +class Overrides { + @Provides @Singleton Wire provideWire() { + return new Wire.LoggingWire().appendToFile("logs/http-wire.log"); + } +} +GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonGitHubModule(), new Overrides()); +``` +#### Pattern Decoders +If you have to only grab a single field from a server response, you may find regular expressions less maintenance than writing a type adapter. + +Here's how our IAM example grabs only one xml element from a response. +```java +@Module(overrides = true, library = true) +static class IAMModule { + @Provides @Singleton Map decoders() { + return ImmutableMap.of("IAM#arn()", Decoders.firstGroup("([\\S&&[^<]]+)")); + } +} +``` + diff --git a/build.gradle b/build.gradle index 5e6c63bb99..f99c728e51 100644 --- a/build.gradle +++ b/build.gradle @@ -23,29 +23,19 @@ apply from: file('gradle/release.gradle') subprojects { group = "com.netflix.${githubProjectName}" // TEMPLATE: Set to organization of project - - dependencies { - compile 'javax.ws.rs:jsr311-api:1.1.1' - compile 'com.sun.jersey:jersey-core:1.11' - testCompile 'org.testng:testng:6.1.1' - testCompile 'org.mockito:mockito-core:1.8.5' - } } -project(':template-client') { +project(':feign-core') { apply plugin: 'java' - dependencies { - compile 'org.slf4j:slf4j-api:1.6.3' - compile 'com.sun.jersey:jersey-client:1.11' - } -} -project(':template-server') { - apply plugin: 'war' - apply plugin: 'jetty' dependencies { - compile 'com.sun.jersey:jersey-server:1.11' - compile 'com.sun.jersey:jersey-servlet:1.11' - compile project(':template-client') - } + compile 'com.google.guava:guava:14.0.1' + compile 'com.squareup.dagger:dagger:1.0.1' + compile 'javax.ws.rs:jsr311-api:1.1.1' + provided 'com.squareup.dagger:dagger-compiler:1.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' + } } diff --git a/feign-core/src/main/java/feign/Client.java b/feign-core/src/main/java/feign/Client.java new file mode 100644 index 0000000000..5659f283c3 --- /dev/null +++ b/feign-core/src/main/java/feign/Client.java @@ -0,0 +1,114 @@ +package feign; + +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.io.ByteSink; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.Reader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import javax.inject.Inject; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLSocketFactory; + +import dagger.Lazy; +import feign.Request.Options; + +import static com.google.common.base.Charsets.UTF_8; +import static com.google.common.net.HttpHeaders.CONTENT_LENGTH; + +/** + * Submits HTTP {@link Request requests}. Implementations are expected to be + * thread-safe. + */ +public interface Client { + /** + * Executes a request against its {@link Request#url() url} and returns a + * response. + * + * @param request safe to replay. + * @param options options to apply to this request. + * @return connected response, {@link Response.Body} is absent or unread. + * @throws IOException on a network error connecting to {@link Request#url()}. + */ + Response execute(Request request, Options options) throws IOException; + + public static class Default implements Client { + private final Lazy sslContextFactory; + + @Inject public Default(Lazy sslContextFactory) { + this.sslContextFactory = sslContextFactory; + } + + @Override public Response execute(Request request, Options options) throws IOException { + HttpURLConnection connection = convertAndSend(request, options); + return convertResponse(connection); + } + + HttpURLConnection convertAndSend(Request request, Options options) throws IOException { + final HttpURLConnection connection = (HttpURLConnection) new URL(request.url()).openConnection(); + if (connection instanceof HttpsURLConnection) { + HttpsURLConnection sslCon = (HttpsURLConnection) connection; + sslCon.setSSLSocketFactory(sslContextFactory.get()); + } + connection.setConnectTimeout(options.connectTimeoutMillis()); + connection.setReadTimeout(options.readTimeoutMillis()); + connection.setAllowUserInteraction(false); + connection.setInstanceFollowRedirects(true); + connection.setRequestMethod(request.method()); + + Integer contentLength = null; + for (Entry header : request.headers().entries()) { + if (header.getKey().equals(CONTENT_LENGTH)) + contentLength = Integer.valueOf(header.getValue()); + connection.addRequestProperty(header.getKey(), header.getValue()); + } + + if (request.body().isPresent()) { + if (contentLength != null) { + connection.setFixedLengthStreamingMode(contentLength); + } else { + connection.setChunkedStreamingMode(8196); + } + connection.setDoOutput(true); + new ByteSink() { + public OutputStream openStream() throws IOException { + return connection.getOutputStream(); + } + }.asCharSink(UTF_8).write(request.body().get()); + } + return connection; + } + + Response convertResponse(HttpURLConnection connection) throws IOException { + int status = connection.getResponseCode(); + String reason = connection.getResponseMessage(); + + ImmutableListMultimap.Builder headers = ImmutableListMultimap.builder(); + for (Map.Entry> field : connection.getHeaderFields().entrySet()) { + // response message + if (field.getKey() != null) + headers.putAll(field.getKey(), field.getValue()); + } + + Integer length = connection.getContentLength(); + if (length == -1) + length = null; + InputStream stream; + if (status >= 400) { + stream = connection.getErrorStream(); + } else { + stream = connection.getInputStream(); + } + Reader body = stream != null ? new InputStreamReader(stream) : null; + return Response.create(status, reason, headers.build(), body, length); + } + } +} diff --git a/feign-core/src/main/java/feign/Contract.java b/feign-core/src/main/java/feign/Contract.java new file mode 100644 index 0000000000..ab30935dbc --- /dev/null +++ b/feign-core/src/main/java/feign/Contract.java @@ -0,0 +1,120 @@ +package feign; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.reflect.TypeToken; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.net.URI; + +import javax.ws.rs.Consumes; +import javax.ws.rs.FormParam; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.HttpMethod; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.net.HttpHeaders.ACCEPT; +import static com.google.common.net.HttpHeaders.CONTENT_TYPE; + +/** + * Defines what annotations and values are valid on interfaces. + */ +public final class Contract { + + public static ImmutableSet parseAndValidatateMetadata(Class declaring) { + ImmutableSet.Builder builder = ImmutableSet.builder(); + for (Method method : declaring.getDeclaredMethods()) { + if (method.getDeclaringClass() == Object.class) + continue; + builder.add(parseAndValidatateMetadata(method)); + } + return builder.build(); + } + + public static MethodMetadata parseAndValidatateMetadata(Method method) { + MethodMetadata data = new MethodMetadata(); + data.returnType(TypeToken.of(method.getGenericReturnType())); + data.configKey(Feign.configKey(method)); + + for (Annotation methodAnnotation : method.getAnnotations()) { + Class annotationType = methodAnnotation.annotationType(); + HttpMethod http = annotationType.getAnnotation(HttpMethod.class); + if (http != null) { + checkState(data.template().method() == null, + "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 == RequestTemplate.Body.class) { + String body = RequestTemplate.Body.class.cast(methodAnnotation).value(); + if (body.indexOf('{') == -1) { + data.template().body(body); + } else { + data.template().bodyTemplate(body); + } + } else if (annotationType == Path.class) { + data.template().append(Path.class.cast(methodAnnotation).value()); + } else if (annotationType == Produces.class) { + data.template().header(CONTENT_TYPE, Joiner.on(',').join(((Produces) methodAnnotation).value())); + } else if (annotationType == Consumes.class) { + data.template().header(ACCEPT, Joiner.on(',').join(((Consumes) methodAnnotation).value())); + } + } + checkState(data.template().method() != null, "Method %s not annotated with HTTP method type (ex. GET, POST)", + method.getName()); + Class[] parameterTypes = method.getParameterTypes(); + + Annotation[][] parameterAnnotationArrays = method.getParameterAnnotations(); + int count = parameterAnnotationArrays.length; + for (int i = 0; i < count; i++) { + boolean hasHttpAnnotation = false; + + Class parameterType = parameterTypes[i]; + Annotation[] parameterAnnotations = parameterAnnotationArrays[i]; + if (parameterAnnotations != null) { + for (Annotation parameterAnnotation : parameterAnnotations) { + Class annotationType = parameterAnnotation.annotationType(); + if (annotationType == PathParam.class) { + data.indexToName().put(i, PathParam.class.cast(parameterAnnotation).value()); + hasHttpAnnotation = true; + } else if (annotationType == QueryParam.class) { + String name = QueryParam.class.cast(parameterAnnotation).value(); + data.template().query( + name, + ImmutableList.builder().addAll(data.template().queries().get(name)) + .add(String.format("{%s}", name)).build()); + data.indexToName().put(i, name); + hasHttpAnnotation = true; + } else if (annotationType == HeaderParam.class) { + String name = HeaderParam.class.cast(parameterAnnotation).value(); + data.template().header( + name, + ImmutableList.builder().addAll(data.template().headers().get(name)) + .add(String.format("{%s}", name)).build()); + data.indexToName().put(i, name); + hasHttpAnnotation = true; + } else if (annotationType == FormParam.class) { + String form = FormParam.class.cast(parameterAnnotation).value(); + data.formParams().add(form); + data.indexToName().put(i, form); + hasHttpAnnotation = true; + } + } + } + + if (parameterType == URI.class) { + data.urlIndex(i); + } else if (!hasHttpAnnotation) { + checkState(data.formParams().isEmpty(), "Body parameters cannot be used with @FormParam parameters."); + checkState(data.bodyIndex() == null, "Method has too many Body parameters: %s", method); + data.bodyIndex(i); + } + } + return data; + } +} \ No newline at end of file diff --git a/feign-core/src/main/java/feign/Feign.java b/feign-core/src/main/java/feign/Feign.java new file mode 100644 index 0000000000..d7a13cda63 --- /dev/null +++ b/feign-core/src/main/java/feign/Feign.java @@ -0,0 +1,148 @@ +package feign; + +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import java.lang.reflect.Method; +import java.util.Map; + +import javax.net.ssl.SSLSocketFactory; + +import dagger.ObjectGraph; +import dagger.Provides; +import feign.Request.Options; +import feign.Target.HardCodedTarget; +import feign.Wire.NoOpWire; +import feign.codec.BodyEncoder; +import feign.codec.Decoder; +import feign.codec.ErrorDecoder; +import feign.codec.FormEncoder; + +/** + * Feign's purpose is to ease development against http apis that feign + * restfulness. + *

+ * In implementation, Feign is a {@link Feign#newInstance factory} for + * generating {@link Target targeted} http apis. + */ +public abstract class Feign { + + /** + * Returns a new instance of an HTTP API, defined by annotations in the + * {@link Feign Contract}, for the specified {@code target}. You should + * cache this result. + */ + public abstract T newInstance(Target target); + + public static T create(Class apiType, String url, Object... modules) { + return create(new HardCodedTarget(apiType, url), modules); + } + + /** + * Shortcut to {@link #newInstance(Target) create} a single {@code targeted} + * http api using {@link ReflectiveFeign reflection}. + */ + public static T create(Target target, Object... modules) { + return create(modules).newInstance(target); + } + + /** + * Returns a {@link ReflectiveFeign reflective} factory for generating + * {@link Target targeted} http apis. + */ + public static Feign create(Object... modules) { + Object[] modulesForGraph = ImmutableList.builder() // + .add(new Defaults()) // + .add(new ReflectiveFeign.Module()) // + .add(Optional.fromNullable(modules).or(new Object[]{})).build().toArray(); + return ObjectGraph.create(modulesForGraph).get(Feign.class); + } + + /** + * Returns an {@link ObjectGraph Dagger ObjectGraph} that can inject a + * {@link ReflectiveFeign reflective} Feign. + */ + public static ObjectGraph createObjectGraph(Object... modules) { + Object[] modulesForGraph = ImmutableList.builder() // + .add(new Defaults()) // + .add(new ReflectiveFeign.Module()) // + .add(Optional.fromNullable(modules).or(new Object[]{})).build().toArray(); + return ObjectGraph.create(modulesForGraph); + } + + @dagger.Module(complete = false, injects = Feign.class, library = true) + public static class Defaults { + + @Provides SSLSocketFactory sslSocketFactory() { + return SSLSocketFactory.class.cast(SSLSocketFactory.getDefault()); + } + + @Provides Client httpClient(Client.Default client) { + return client; + } + + @Provides Retryer retryer() { + return new Retryer.Default(); + } + + @Provides Wire noOp() { + return new NoOpWire(); + } + + @Provides Map noOptions() { + return ImmutableMap.of(); + } + + @Provides Map noBodyEncoders() { + return ImmutableMap.of(); + } + + @Provides Map noFormEncoders() { + return ImmutableMap.of(); + } + + @Provides Map noDecoders() { + return ImmutableMap.of(); + } + + @Provides Map noErrorDecoders() { + return ImmutableMap.of(); + } + } + + /** + *

+ * For example. + *

    + *
  • {@code Route53}: would match a class such as + * {@code denominator.route53.Route53} + *
  • {@code Route53#list()}: would match a method such as + * {@code denominator.route53.Route53#list()} + *
  • {@code Route53#listAt(Marker)}: would match a method such as + * {@code denominator.route53.Route53#listAt(denominator.route53.Marker)} + *
  • {@code Route53#listByNameAndType(String, String)}: would match a + * method such as {@code denominator.route53.Route53#listAt(String, String)} + *
+ *

+ * Note that there is no whitespace expected in a key! + */ + public static String configKey(Method method) { + StringBuilder builder = new StringBuilder(); + builder.append(method.getDeclaringClass().getSimpleName()); + builder.append('#').append(method.getName()).append('('); + for (Class param : method.getParameterTypes()) + builder.append(param.getSimpleName()).append(','); + if (method.getParameterTypes().length > 0) + builder.deleteCharAt(builder.length() - 1); + return builder.append(')').toString(); + } + + Feign() { + + } +} \ No newline at end of file diff --git a/feign-core/src/main/java/feign/FeignException.java b/feign-core/src/main/java/feign/FeignException.java new file mode 100644 index 0000000000..2375c5fa96 --- /dev/null +++ b/feign-core/src/main/java/feign/FeignException.java @@ -0,0 +1,51 @@ +package feign; + +import com.google.common.reflect.TypeToken; + +import java.io.IOException; + +import feign.codec.Decoder; +import feign.codec.ToStringDecoder; + +import static java.lang.String.format; + +/** + * Origin exception type for all HttpApis. + */ +public class FeignException extends RuntimeException { + static FeignException errorReading(Request request, Response response, IOException cause) { + return new FeignException(format("%s %s %s", cause.getMessage(), request.method(), request.url(), 0), cause); + } + + private static final Decoder toString = new ToStringDecoder(); + private static final TypeToken stringToken = TypeToken.of(String.class); + + public static FeignException errorStatus(String methodKey, Response response) { + String message = format("status %s reading %s", response.status(), methodKey); + try { + Object body = toString.decode(methodKey, response, stringToken); + if (body != null) { + response = Response.create(response.status(), response.reason(), response.headers(), body.toString()); + message += "; content:\n" + body; + } + } catch (IOException ignored) { + + } + return new FeignException(message); + } + + static FeignException errorExecuting(Request request, IOException cause) { + return new RetryableException(format("error %s executing %s %s", cause.getMessage(), request.method(), + request.url()), cause, null); + } + + protected FeignException(String message, Throwable cause) { + super(message, cause); + } + + protected FeignException(String message) { + super(message); + } + + private static final long serialVersionUID = 0; +} \ No newline at end of file diff --git a/feign-core/src/main/java/feign/MethodHandler.java b/feign-core/src/main/java/feign/MethodHandler.java new file mode 100644 index 0000000000..a3a84b3d1e --- /dev/null +++ b/feign-core/src/main/java/feign/MethodHandler.java @@ -0,0 +1,127 @@ +package feign; + +import com.google.common.base.Function; +import com.google.common.collect.ImmutableList; +import com.google.common.reflect.TypeToken; + +import java.io.IOException; +import java.net.URI; + +import javax.inject.Inject; +import javax.inject.Provider; + +import feign.Request.Options; +import feign.codec.Decoder; +import feign.codec.ErrorDecoder; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.net.HttpHeaders.LOCATION; +import static feign.FeignException.errorExecuting; +import static feign.FeignException.errorReading; + +final class MethodHandler { + + static class Factory { + + private final Client client; + private final Provider retryer; + private final Wire wire; + + @Inject Factory(Client client, Provider retryer, Wire wire) { + this.client = checkNotNull(client, "client"); + this.retryer = checkNotNull(retryer, "retryer"); + this.wire = checkNotNull(wire, "wire"); + } + + public MethodHandler create(Target target, MethodMetadata md, + Function buildTemplateFromArgs, Options options, Decoder decoder, ErrorDecoder errorDecoder) { + return new MethodHandler(target, client, retryer, wire, md, buildTemplateFromArgs, options, decoder, errorDecoder); + } + } + + private final MethodMetadata metadata; + private final Target target; + private final Client client; + private final Provider retryer; + private final Wire wire; + + private final Function buildTemplateFromArgs; + private final Options options; + private final Decoder decoder; + private final ErrorDecoder errorDecoder; + + // cannot inject wildcards in dagger + @SuppressWarnings("rawtypes") + private MethodHandler(Target target, Client client, Provider retryer, Wire wire, MethodMetadata metadata, + Function buildTemplateFromArgs, Options options, Decoder decoder, ErrorDecoder errorDecoder) { + this.target = checkNotNull(target, "target"); + this.client = checkNotNull(client, "client for %s", target); + this.retryer = checkNotNull(retryer, "retryer for %s", target); + this.wire = checkNotNull(wire, "wire for %s", target); + this.metadata = checkNotNull(metadata, "metadata for %s", target); + this.buildTemplateFromArgs = checkNotNull(buildTemplateFromArgs, "metadata for %s", target); + this.options = checkNotNull(options, "options for %s", target); + this.decoder = checkNotNull(decoder, "decoder for %s", target); + this.errorDecoder = checkNotNull(errorDecoder, "errorDecoder for %s", target); + } + + public Object invoke(Object[] argv) throws Throwable { + RequestTemplate template = buildTemplateFromArgs.apply(argv); + Retryer retryer = this.retryer.get(); + while (true) { + try { + return executeAndDecode(metadata.configKey(), template, metadata.returnType()); + } catch (RetryableException e) { + retryer.continueOrPropagate(e); + continue; + } + } + } + + public Object executeAndDecode(String configKey, RequestTemplate template, TypeToken returnType) + throws Throwable { + // create the request from a mutable copy of the input template. + Request request = target.apply(new RequestTemplate(template)); + wire.wireRequest(target, request); + Response response = execute(request); + try { + response = wire.wireAndRebufferResponse(target, response); + if (response.status() >= 200 && response.status() < 300) { + if (returnType.getRawType().equals(Response.class)) { + return response; + } else if (returnType.getRawType() == URI.class && !response.body().isPresent()) { + ImmutableList location = response.headers().get(LOCATION); + if (!location.isEmpty()) + return URI.create(location.get(0)); + } else if (returnType.getRawType() == void.class) { + return null; + } + return decoder.decode(configKey, response, returnType); + } else { + return errorDecoder.decode(configKey, response, returnType); + } + } catch (Throwable e) { + ensureBodyClosed(response); + if (IOException.class.isInstance(e)) + throw errorReading(request, response, IOException.class.cast(e)); + throw e; + } + } + + private void ensureBodyClosed(Response response) { + if (response.body().isPresent()) { + try { + response.body().get().close(); + } catch (IOException ignored) { + } + } + } + + private Response execute(Request request) { + try { + return client.execute(request, options); + } catch (IOException e) { + throw errorExecuting(request, e); + } + } +} diff --git a/feign-core/src/main/java/feign/MethodMetadata.java b/feign-core/src/main/java/feign/MethodMetadata.java new file mode 100644 index 0000000000..409c5e624b --- /dev/null +++ b/feign-core/src/main/java/feign/MethodMetadata.java @@ -0,0 +1,76 @@ +package feign; + +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.Lists; +import com.google.common.collect.SetMultimap; +import com.google.common.reflect.TypeToken; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.List; + +public final class MethodMetadata implements Serializable { + MethodMetadata() { + } + + private String configKey; + private transient TypeToken returnType; + private Integer urlIndex; + private Integer bodyIndex; + private RequestTemplate template = new RequestTemplate(); + private List formParams = Lists.newArrayList(); + private SetMultimap indexToName = LinkedHashMultimap.create(); + + /** + * @see Feign#configKey(Method) + */ + public String configKey() { + return configKey; + } + + MethodMetadata configKey(String configKey) { + this.configKey = configKey; + return this; + } + + public TypeToken returnType() { + return returnType; + } + + MethodMetadata returnType(TypeToken returnType) { + this.returnType = returnType; + return this; + } + + public Integer urlIndex() { + return urlIndex; + } + + MethodMetadata urlIndex(Integer urlIndex) { + this.urlIndex = urlIndex; + return this; + } + + public Integer bodyIndex() { + return bodyIndex; + } + + MethodMetadata bodyIndex(Integer bodyIndex) { + this.bodyIndex = bodyIndex; + return this; + } + + public RequestTemplate template() { + return template; + } + + public List formParams() { + return formParams; + } + + public SetMultimap indexToName() { + return indexToName; + } + + private static final long serialVersionUID = 1L; +} \ No newline at end of file diff --git a/feign-core/src/main/java/feign/ReflectiveFeign.java b/feign-core/src/main/java/feign/ReflectiveFeign.java new file mode 100644 index 0000000000..c65b885aca --- /dev/null +++ b/feign-core/src/main/java/feign/ReflectiveFeign.java @@ -0,0 +1,247 @@ +package feign; + +import com.google.common.base.Function; +import com.google.common.base.Objects; +import com.google.common.base.Predicates; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMap.Builder; +import com.google.common.collect.Maps; +import com.google.common.reflect.AbstractInvocationHandler; +import com.google.common.reflect.Reflection; + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import javax.inject.Inject; + +import dagger.Provides; +import feign.MethodHandler.Factory; +import feign.Request.Options; +import feign.codec.BodyEncoder; +import feign.codec.Decoder; +import feign.codec.ErrorDecoder; +import feign.codec.FormEncoder; +import feign.codec.ToStringDecoder; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static feign.Contract.parseAndValidatateMetadata; +import static java.lang.String.format; + +@SuppressWarnings("rawtypes") +public class ReflectiveFeign extends Feign { + + private final Function> targetToHandlersByName; + + @Inject ReflectiveFeign(Function> targetToHandlersByName) { + this.targetToHandlersByName = targetToHandlersByName; + } + + /** + * creates an api binding to the {@code target}. As this invokes reflection, + * care should be taken to cache the result. + */ + @Override public T newInstance(Target target) { + Map nameToHandler = targetToHandlersByName.apply(target); + Builder methodToHandler = ImmutableMap.builder(); + for (Method method : target.type().getDeclaredMethods()) { + if (method.getDeclaringClass() == Object.class) + continue; + methodToHandler.put(method, nameToHandler.get(Feign.configKey(method))); + } + FeignInvocationHandler handler = new FeignInvocationHandler(target, methodToHandler.build()); + return Reflection.newProxy(target.type(), handler); + } + + static class FeignInvocationHandler extends AbstractInvocationHandler { + + private final Target target; + private final Map methodToHandler; + + FeignInvocationHandler(Target target, ImmutableMap methodToHandler) { + this.target = checkNotNull(target, "target"); + this.methodToHandler = checkNotNull(methodToHandler, "methodToHandler for %s", target); + } + + @Override protected Object handleInvocation(Object proxy, Method method, Object[] args) throws Throwable { + return methodToHandler.get(method).invoke(args); + } + + @Override public int hashCode() { + return target.hashCode(); + } + + @Override public boolean equals(Object obj) { + if (this == obj) + return true; + if (FeignInvocationHandler.class != obj.getClass()) + return false; + FeignInvocationHandler that = FeignInvocationHandler.class.cast(obj); + return this.target.equals(that.target); + } + + @Override public String toString() { + return Objects.toStringHelper("").add("name", target.name()).add("url", target.url()).toString(); + } + } + + @dagger.Module(complete = false,// Config + injects = Feign.class, library = true// provides Feign + ) + public static class Module { + + @Provides Feign provideFeign(ReflectiveFeign in) { + return in; + } + + @Provides Function> targetToHandlersByName(ParseHandlersByName parseHandlersByName) { + return parseHandlersByName; + } + } + + 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 implements Function> { + private final Map options; + private final Map bodyEncoders; + private final Map formEncoders; + private final Map decoders; + private final Map errorDecoders; + private final Factory factory; + + @Inject ParseHandlersByName(Map options, Map bodyEncoders, + Map formEncoders, Map decoders, + Map errorDecoders, Factory factory) { + this.options = options; + this.bodyEncoders = bodyEncoders; + this.formEncoders = formEncoders; + this.decoders = decoders; + this.factory = factory; + this.errorDecoders = errorDecoders; + } + + @Override public Map apply(Target key) { + Set metadata = parseAndValidatateMetadata(key.type()); + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (MethodMetadata md : metadata) { + Options options = forMethodOrClass(this.options, md.configKey()); + if (options == null) { + options = new Options(); + } + Decoder decoder = forMethodOrClass(decoders, md.configKey()); + if (decoder == null + && (md.returnType().getRawType() == void.class || md.returnType().getRawType() == Response.class)) { + decoder = new ToStringDecoder(); + } + if (decoder == null) { + throw noConfig(md.configKey(), Decoder.class); + } + ErrorDecoder errorDecoder = forMethodOrClass(errorDecoders, md.configKey()); + if (errorDecoder == null) { + errorDecoder = ErrorDecoder.DEFAULT; + } + Function buildTemplateFromArgs; + if (!md.formParams().isEmpty() && !md.template().bodyTemplate().isPresent()) { + FormEncoder formEncoder = forMethodOrClass(formEncoders, md.configKey()); + if (formEncoder == null) { + throw noConfig(md.configKey(), FormEncoder.class); + } + buildTemplateFromArgs = new BuildFormEncodedTemplateFromArgs(md, formEncoder); + } else if (md.bodyIndex() != null) { + BodyEncoder bodyEncoder = forMethodOrClass(bodyEncoders, md.configKey()); + if (bodyEncoder == null) { + throw noConfig(md.configKey(), BodyEncoder.class); + } + buildTemplateFromArgs = new BuildBodyEncodedTemplateFromArgs(md, bodyEncoder); + } else { + buildTemplateFromArgs = new BuildTemplateFromArgs(md); + } + builder.put(md.configKey(), + factory.create(key, md, buildTemplateFromArgs, options, decoder, errorDecoder)); + } + return builder.build(); + } + } + + private static class BuildTemplateFromArgs implements Function { + protected final MethodMetadata metadata; + + private BuildTemplateFromArgs(MethodMetadata metadata) { + this.metadata = metadata; + } + + @Override + public RequestTemplate apply(Object[] argv) { + RequestTemplate mutable = new RequestTemplate(metadata.template()); + if (metadata.urlIndex() != null) { + int urlIndex = metadata.urlIndex(); + checkArgument(argv[urlIndex] != null, "URI parameter %s was null", urlIndex); + mutable.insert(0, String.valueOf(argv[urlIndex])); + } + ImmutableMap.Builder varBuilder = ImmutableMap.builder(); + for (Entry> entry : metadata.indexToName().asMap().entrySet()) { + Object value = argv[entry.getKey()]; + if (value != null) { // Null values are skipped. + for (String name : entry.getValue()) + varBuilder.put(name, value); + } + } + return resolve(argv, mutable, varBuilder.build()); + } + + protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, ImmutableMap variables) { + return mutable.resolve(variables); + } + } + + private static class BuildFormEncodedTemplateFromArgs extends BuildTemplateFromArgs { + private final FormEncoder formEncoder; + + private BuildFormEncodedTemplateFromArgs(MethodMetadata metadata, FormEncoder formEncoder) { + super(metadata); + this.formEncoder = formEncoder; + } + + @Override protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, ImmutableMap variables) { + formEncoder.encodeForm(Maps.filterKeys(variables, Predicates.in(metadata.formParams())), mutable); + return super.resolve(argv, mutable, variables); + } + } + + private static class BuildBodyEncodedTemplateFromArgs extends BuildTemplateFromArgs { + private final BodyEncoder bodyEncoder; + + private BuildBodyEncodedTemplateFromArgs(MethodMetadata metadata, BodyEncoder bodyEncoder) { + super(metadata); + this.bodyEncoder = bodyEncoder; + } + + @Override protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, ImmutableMap variables) { + Object body = argv[metadata.bodyIndex()]; + checkArgument(body != null, "Body parameter %s was null", metadata.bodyIndex()); + bodyEncoder.encodeBody(body, mutable); + return super.resolve(argv, mutable, variables); + } + } + + static T forMethodOrClass(Map config, String configKey) { + if (config.containsKey(configKey)) { + return config.get(configKey); + } + String classKey = toClassKey(configKey); + if (config.containsKey(classKey)) { + return config.get(classKey); + } + return null; + } + + public static String toClassKey(String methodKey) { + return methodKey.substring(0, methodKey.indexOf('#')); + } +} diff --git a/feign-core/src/main/java/feign/Request.java b/feign-core/src/main/java/feign/Request.java new file mode 100644 index 0000000000..30a47d966c --- /dev/null +++ b/feign-core/src/main/java/feign/Request.java @@ -0,0 +1,113 @@ +package feign; + +import com.google.common.base.Objects; +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableListMultimap; + +import java.util.Map.Entry; + +import static com.google.common.base.Objects.equal; +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * An immutable request to an http server. + *

+ *

Note

+ *

+ * Since {@link Feign} is designed for non-binary apis, and expectations are + * that any request can be replayed, we only support a String body. + */ +public final class Request { + + private final String method; + private final String url; + private final ImmutableListMultimap headers; + private final Optional body; + + Request(String method, String url, ImmutableListMultimap headers, Optional body) { + this.method = checkNotNull(method, "method of %s", url); + this.url = checkNotNull(url, "url"); + this.headers = checkNotNull(headers, "headers of %s %s", method, url); + this.body = checkNotNull(body, "body of %s %s", method, url); + } + + /* Method to invoke on the server. */ + public String method() { + return method; + } + + /* Fully resolved URL including query. */ + public String url() { + return url; + } + + /* Ordered list of headers that will be sent to the server. */ + public ImmutableListMultimap headers() { + return headers; + } + + /* If present, this is the replayable body to send to the server. */ + public Optional body() { + return body; + } + + /* Controls the per-request settings currently required to be implemented by all {@link Client clients} */ + public static class Options { + + private final int connectTimeoutMillis; + private final int readTimeoutMillis; + + public Options(int connectTimeoutMillis, int readTimeoutMillis) { + this.connectTimeoutMillis = connectTimeoutMillis; + this.readTimeoutMillis = readTimeoutMillis; + } + + public Options() { + this(10 * 1000, 60 * 1000); + } + + /** + * Defaults to 10 seconds. {@code 0} implies no timeout. + * + * @see java.net.HttpURLConnection#getConnectTimeout() + */ + public int connectTimeoutMillis() { + return connectTimeoutMillis; + } + + /** + * Defaults to 60 seconds. {@code 0} implies no timeout. + * + * @see java.net.HttpURLConnection#getReadTimeout() + */ + public int readTimeoutMillis() { + return readTimeoutMillis; + } + } + + @Override public int hashCode() { + return Objects.hashCode(method, url, headers, body); + } + + @Override public boolean equals(Object obj) { + if (this == obj) + return true; + if (Request.class != obj.getClass()) + return false; + Request that = Request.class.cast(obj); + return equal(this.method, that.method) && equal(this.url, that.url) && equal(this.headers, that.headers) + && equal(this.body, that.body); + } + + @Override public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(method).append(' ').append(url).append(" HTTP/1.1\n"); + for (Entry header : headers.entries()) { + builder.append(header.getKey()).append(": ").append(header.getValue()).append('\n'); + } + if (body.isPresent()) { + builder.append('\n').append(body.get()); + } + return builder.toString(); + } +} diff --git a/feign-core/src/main/java/feign/RequestTemplate.java b/feign-core/src/main/java/feign/RequestTemplate.java new file mode 100644 index 0000000000..04922447f0 --- /dev/null +++ b/feign-core/src/main/java/feign/RequestTemplate.java @@ -0,0 +1,533 @@ +package feign; + +import com.google.common.base.Objects; +import com.google.common.base.Optional; +import com.google.common.base.Splitter; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.Iterables; +import com.google.common.collect.LinkedListMultimap; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; +import com.google.common.collect.Multimaps; +import com.google.common.net.HttpHeaders; + +import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.util.Collection; +import java.util.Map; +import java.util.Map.Entry; + +import feign.codec.BodyEncoder; +import feign.codec.FormEncoder; + +import static com.google.common.base.Charsets.UTF_8; +import static com.google.common.base.Objects.equal; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.net.HttpHeaders.CONTENT_LENGTH; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Builds a request to an http target. Not thread safe. + *

+ *

relationship to JAXRS 2.0

+ *

+ * A combination of {@code javax.ws.rs.client.WebTarget} and + * {@code javax.ws.rs.client.Invocation.Builder}, ensuring you can modify any + * part of the request. However, this object is mutable, so needs to be guarded + * with the copy constructor. + */ +public final class RequestTemplate implements Serializable { + + /** + * A templatized form for a PUT or POST command. Values of {@link javax.ws.rs.PathParam}, + * {@link javax.ws.rs.QueryParam}, {@link javax.ws.rs.HeaderParam}, and {@link javax.ws.rs.FormParam} can be + * used are passed to the template. + *

+ * ex. + *

+ *

+   * @Body("<v01:getResourceRecordsOfZone><zoneName>{zoneName}</zoneName><rrType>0</rrType></v01:getResourceRecordsOfZone>")
+   * List<Record> listByZone(@PayloadParam("zoneName") String zoneName);
+   * 
+ *

+ * Note that if you'd like curly braces literally in the body, urlencode + * them first. + * + * @see RequestTemplate#expand(String, Map) + */ + @Target(METHOD) @Retention(RUNTIME) public @interface Body { + String value(); + } + + private String method; + /* final to encourage mutable use vs replacing the object. */ + private StringBuilder url = new StringBuilder(); + private final ListMultimap queries = LinkedListMultimap.create(); + private final ListMultimap headers = LinkedListMultimap.create(); + private Optional body = Optional.absent(); + private Optional bodyTemplate = Optional.absent(); + + public RequestTemplate() { + + } + + /* Copy constructor. Use this when making templates. */ + public RequestTemplate(RequestTemplate toCopy) { + checkNotNull(toCopy, "toCopy"); + this.method = toCopy.method; + this.url.append(toCopy.url); + this.queries.putAll(toCopy.queries); + this.headers.putAll(toCopy.headers); + this.body = toCopy.body; + this.bodyTemplate = toCopy.bodyTemplate; + } + + /** + * Targets a template to this target, adding the {@link #url() base url} and + * any authentication headers. + *

+ *

+ * For example: + *

+ *

+   * public Request apply(RequestTemplate input) {
+   *     input.insert(0, url());
+   *     input.replaceHeader("X-Auth", currentToken);
+   *     return input.asRequest();
+   * }
+   * 
+ *

+ *

relationship to JAXRS 2.0

+ *

+ * This call is similar to + * {@code javax.ws.rs.client.WebTarget.resolveTemplates(templateValues, true)} + * , except that the template values apply to any part of the request, not + * just the URL + */ + public RequestTemplate resolve(Map unencoded) { + Map encoded = Maps.newLinkedHashMap(); + for (Entry 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); + + ListMultimap resolvedHeaders = LinkedListMultimap.create(); + for (Entry entry : headers.entries()) { + String value = null; + if (entry.getValue().indexOf('{') == 0) { + value = String.valueOf(unencoded.get(entry.getKey())); + } else { + value = entry.getValue(); + } + if (value != null) + resolvedHeaders.put(entry.getKey(), value); + } + headers.clear(); + headers.putAll(resolvedHeaders); + if (bodyTemplate.isPresent()) + body(urlDecode(expand(bodyTemplate.get(), unencoded))); + return this; + } + + /* roughly analogous to {@code javax.ws.rs.client.Target.request()}. */ + public Request request() { + return new Request(method, new StringBuilder(url).append(queryLine()).toString(), + ImmutableListMultimap.copyOf(headers), body); + } + + private static String urlDecode(String arg) { + try { + return URLDecoder.decode(arg, UTF_8.name()); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + private static String urlEncode(Object arg) { + try { + return URLEncoder.encode(String.valueOf(arg), UTF_8.name()); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + /** + * Expands a {@code template}, such as {@code username} + * }, using the {@code variables} supplied. Any unresolved + * parameters will remain. + *

+ * Note that if you'd like curly braces literally in the {@code template}, + * urlencode them first. + * + * @param template URI template that can be in level 1 RFC6570 form. + * @param variables to the URI template + * @return expanded template, leaving any unresolved parameters literal + */ + public static String expand(String template, Map variables) { + // skip expansion if there's no valid variables set. ex. {a} is the + // first valid + if (checkNotNull(template, "template").length() < 3) + return template.toString(); + checkNotNull(variables, "variables for %s", template); + + boolean inVar = false; + StringBuilder var = new StringBuilder(); + StringBuilder builder = new StringBuilder(); + for (char c : Lists.charactersOf(template)) { + switch (c) { + case '{': + inVar = true; + break; + case '}': + inVar = false; + String key = var.toString(); + Object value = variables.get(var.toString()); + if (value != null) + builder.append(value); + else + builder.append('{').append(key).append('}'); + var = new StringBuilder(); + break; + default: + if (inVar) + var.append(c); + else + builder.append(c); + } + } + return builder.toString(); + } + + /* @see Request#method() */ + public RequestTemplate method(String method) { + this.method = checkNotNull(method, "method"); + return this; + } + + /* @see Request#method() */ + public String method() { + return method; + } + + /* @see #url() */ + public RequestTemplate append(CharSequence value) { + url.append(value); + url = pullAnyQueriesOutOfUrl(url); + return this; + } + + /* @see #url() */ + public RequestTemplate insert(int pos, CharSequence value) { + url.insert(pos, value); + url = pullAnyQueriesOutOfUrl(url); + return this; + } + + public String url() { + return url.toString(); + } + + /** + * Replaces queries with the specified {@code configKey} with url decoded + * {@code values} supplied. + *

+ * When the {@code value} is {@code null}, all queries with the {@code configKey} + * are removed. + *

+ *

relationship to JAXRS 2.0

+ *

+ * Like {@code WebTarget.query}, except the values can be templatized. + *

+ * ex. + *

+ *

+   * template.query("Signature", "{signature}");
+   * 
+ * + * @param configKey the configKey of the query + * @param values can be a single null to imply removing all values. Else no + * values are expected to be null. + * @see #queries() + */ + public RequestTemplate query(String configKey, String... values) { + queries.removeAll(checkNotNull(configKey, "configKey")); + if (values != null && values.length > 0 && values[0] != null) { + for (String value : values) + this.queries.put(encodeIfNotVariable(configKey), encodeIfNotVariable(value)); + } + return this; + } + + /* @see #query(String, String...) */ + public RequestTemplate query(String configKey, Iterable values) { + if (values != null) + return query(configKey, Iterables.toArray(values, String.class)); + return query(configKey, (String[]) null); + } + + private String encodeIfNotVariable(String in) { + if (in == null || in.indexOf('{') == 0) + return in; + return urlEncode(in); + } + + /** + * Replaces all existing queries with the newly supplied url decoded + * queries. + *

+ *

relationship to JAXRS 2.0

+ *

+ * Like {@code WebTarget.queries}, except the values can be templatized. + *

+ * ex. + *

+ *

+   * template.queries(ImmutableMultimap.of("Signature", "{signature}"));
+   * 
+ * + * @param queries if null, remove all queries. else value to replace all queries + * with. + * @see #queries() + */ + public RequestTemplate queries(Multimap queries) { + if (queries == null || queries.isEmpty()) { + this.queries.clear(); + } else { + for (Entry> entry : queries.asMap().entrySet()) + query(entry.getKey(), Iterables.toArray(entry.getValue(), String.class)); + } + return this; + } + + /** + * Returns an immutable copy of the url decoded queries. + * + * @see Request#url() + */ + public ListMultimap queries() { + ListMultimap unencoded = LinkedListMultimap.create(); + for (Entry entry : queries.entries()) + unencoded.put(urlDecode(entry.getKey()), urlDecode(entry.getValue())); + return Multimaps.unmodifiableListMultimap(unencoded); + } + + /** + * Replaces headers with the specified {@code configKey} with the + * {@code values} supplied. + *

+ * When the {@code value} is {@code null}, all headers with the {@code configKey} + * are removed. + *

+ *

relationship to JAXRS 2.0

+ *

+ * Like {@code WebTarget.queries} and {@code javax.ws.rs.client.Invocation.Builder.header}, + * except the values can be templatized. + *

+ * ex. + *

+ *

+   * template.query("X-Application-Version", "{version}");
+   * 
+ * + * @param configKey the configKey of the header + * @param values can be a single null to imply removing all values. Else no + * values are expected to be null. + * @see #headers() + */ + public RequestTemplate header(String configKey, String... values) { + checkNotNull(configKey, "header configKey"); + if (values == null || (values.length == 1 && values[0] == null)) + headers.removeAll(configKey); + else + this.headers.replaceValues(configKey, ImmutableList.copyOf(values)); + return this; + } + + /* @see #header(String, String...) */ + public RequestTemplate header(String configKey, Iterable values) { + if (values != null) + return header(configKey, Iterables.toArray(values, String.class)); + return header(configKey, (String[]) null); + } + + /** + * Replaces all existing headers with the newly supplied headers. + *

+ *

relationship to JAXRS 2.0

+ *

+ * Like {@code Invocation.Builder.headers(MultivaluedMap)}, except the + * values can be templatized. + *

+ * ex. + *

+ *

+   * template.headers(ImmutableMultimap.of("X-Application-Version", "{version}"));
+   * 
+ * + * @param headers if null, remove all headers. else value to replace all headers + * with. + * @see #headers() + */ + public RequestTemplate headers(Multimap headers) { + if (headers == null || headers.isEmpty()) + this.headers.clear(); + else + this.headers.putAll(headers); + return this; + } + + /** + * Returns an immutable copy of the current headers. + * + * @see Request#headers() + */ + public ListMultimap headers() { + return ImmutableListMultimap.copyOf(headers); + } + + /** + * replaces the {@link HttpHeaders#CONTENT_LENGTH} header. + *

+ * Usually populated by {@link BodyEncoder} or {@link FormEncoder} + * + * @see Request#body() + */ + public RequestTemplate body(String body) { + this.body = Optional.fromNullable(body); + if (this.body.isPresent()) { + byte[] contentLength = body.getBytes(UTF_8); + header(CONTENT_LENGTH, String.valueOf(contentLength.length)); + } + this.bodyTemplate = Optional.absent(); + return this; + } + + /* @see Request#body() */ + public Optional body() { + return body; + } + + /** + * populated by {@link Body} + * + * @see Request#body() + */ + public RequestTemplate bodyTemplate(String bodyTemplate) { + this.bodyTemplate = Optional.fromNullable(bodyTemplate); + this.body = Optional.absent(); + return this; + } + + /** + * @see Request#body() + * @see #expand(String, Map) + */ + public Optional bodyTemplate() { + return bodyTemplate; + } + + @Override public int hashCode() { + return Objects.hashCode(method, url, queries, headers, body); + } + + /** + * if there are any query params in the {@link #body()}, this will extract + * them out. + * + * @return + */ + private StringBuilder pullAnyQueriesOutOfUrl(StringBuilder url) { + // parse out queries + int queryIndex = url.indexOf("?"); + if (queryIndex != -1) { + String queryLine = url.substring(queryIndex + 1); + ListMultimap firstQueries = parseAndDecodeQueries(queryLine); + if (!queries.isEmpty()) { + firstQueries.putAll(queries); + queries.clear(); + } + queries.putAll(firstQueries); + return new StringBuilder(url.substring(0, queryIndex)); + } + return url; + } + + private static ListMultimap parseAndDecodeQueries(String queryLine) { + ListMultimap map = LinkedListMultimap.create(); + if (Strings.emptyToNull(queryLine) == null) + return map; + if (queryLine.indexOf('&') == -1) { + if (queryLine.indexOf('=') != -1) + putKV(queryLine, map); + else + map.put(queryLine, null); + } else { + for (String part : Splitter.on('&').split(queryLine)) { + putKV(part, map); + } + } + return map; + } + + private static void putKV(String stringToParse, Multimap map) { + String key; + String value; + // note that '=' can be a valid part of the value + int firstEq = stringToParse.indexOf('='); + if (firstEq == -1) { + key = urlDecode(stringToParse); + value = null; + } else { + key = urlDecode(stringToParse.substring(0, firstEq)); + value = urlDecode(stringToParse.substring(firstEq + 1)); + } + map.put(key, value); + } + + @Override public boolean equals(Object obj) { + if (this == obj) + return true; + if (RequestTemplate.class != obj.getClass()) + return false; + RequestTemplate that = RequestTemplate.class.cast(obj); + return equal(this.method, that.method) && equal(this.url, that.url) && equal(this.queries, that.queries) + && equal(this.headers, that.headers) && equal(this.body, that.body); + } + + @Override public String toString() { + return request().toString(); + } + + public String queryLine() { + if (queries.isEmpty()) + return ""; + StringBuilder queryBuilder = new StringBuilder(); + for (Entry pair : queries.entries()) { + queryBuilder.append('&'); + queryBuilder.append(pair.getKey()); + if (pair.getValue() != null) + queryBuilder.append('='); + if (pair.getValue() != null && !pair.getValue().equals("")) { + queryBuilder.append(pair.getValue()); + } + } + queryBuilder.deleteCharAt(0); + return queryBuilder.insert(0, '?').toString(); + } + + private static final long serialVersionUID = 1L; +} \ No newline at end of file diff --git a/feign-core/src/main/java/feign/Response.java b/feign-core/src/main/java/feign/Response.java new file mode 100644 index 0000000000..6b68681a45 --- /dev/null +++ b/feign-core/src/main/java/feign/Response.java @@ -0,0 +1,183 @@ +package feign; + +import com.google.common.base.Objects; +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableListMultimap; + +import java.io.Closeable; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.util.Map.Entry; + +import static com.google.common.base.Charsets.UTF_8; +import static com.google.common.base.Objects.equal; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +/** + * An immutable response to an http invocation which only returns string + * content. + */ +public final class Response { + private final int status; + private final String reason; + private final ImmutableListMultimap headers; + private final Optional body; + + public static Response create(int status, String reason, ImmutableListMultimap headers, + Reader chars, Integer length) { + return new Response(status, reason, headers, Optional.fromNullable(ReaderBody.orNull(chars, length))); + } + + public static Response create(int status, String reason, ImmutableListMultimap headers, String chars) { + return new Response(status, reason, headers, Optional.fromNullable(StringBody.orNull(chars))); + } + + private Response(int status, String reason, ImmutableListMultimap headers, Optional body) { + checkState(status >= 200, "Invalid status code: %s", status); + this.status = status; + this.reason = checkNotNull(reason, "reason"); + this.headers = checkNotNull(headers, "headers"); + this.body = checkNotNull(body, "body"); + } + + /** + * status code. ex {@code 200} + * + * @see + */ + public int status() { + return status; + } + + public String reason() { + return reason; + } + + public ImmutableListMultimap headers() { + return headers; + } + + public Optional body() { + return body; + } + + public static interface Body extends Closeable { + + /** + * length in bytes, if known. + *

+ *

Note

This is an integer as most implementations cannot do + * bodies > 2GB. Moreover, the scope of this interface doesn't include + * large bodies. + */ + Optional length(); + + /** + * True if {@link #asReader()} can be called more than once. + */ + boolean isRepeatable(); + + /** + * It is the responsibility of the caller to close the stream. + */ + Reader asReader() throws IOException; + } + + private static final class ReaderBody implements Response.Body { + private static Body orNull(Reader chars, Integer length) { + if (chars == null) + return null; + return new ReaderBody(chars, Optional.fromNullable(length)); + } + + private final Reader chars; + private final Optional length; + + private ReaderBody(Reader chars, Optional length) { + this.chars = chars; + this.length = length; + } + + @Override public Optional length() { + return length; + } + + @Override public boolean isRepeatable() { + return false; + } + + @Override public Reader asReader() throws IOException { + return chars; + } + + @Override public void close() throws IOException { + chars.close(); + } + } + + private static final class StringBody implements Response.Body { + private static Body orNull(String chars) { + if (chars == null) + return null; + return new StringBody(chars); + } + + private final String chars; + + public StringBody(String chars) { + this.chars = chars; + } + + private volatile Optional length; + + @Override public Optional length() { + if (length == null) { + length = Optional.of(chars.getBytes(UTF_8).length); + } + return length; + } + + @Override public boolean isRepeatable() { + return true; + } + + @Override public Reader asReader() throws IOException { + return new StringReader(chars); + } + + public String toString() { + return chars; + } + + @Override public void close() { + } + } + + @Override public int hashCode() { + return Objects.hashCode(status, reason, headers, body); + } + + @Override public boolean equals(Object obj) { + if (this == obj) + return true; + if (Response.class != obj.getClass()) + return false; + Response that = Response.class.cast(obj); + return equal(this.status, that.status) && equal(this.reason, that.reason) && equal(this.headers, that.headers) + && equal(this.body, that.body); + } + + @Override public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("HTTP/1.1 ").append(status).append(' ').append(reason).append('\n'); + for (Entry header : headers.entries()) { + builder.append(header.getKey()).append(": ").append(header.getValue()).append('\n'); + } + if (body.isPresent()) { + builder.append('\n').append(body.get()); + } + return builder.toString(); + } +} diff --git a/feign-core/src/main/java/feign/RetryableException.java b/feign-core/src/main/java/feign/RetryableException.java new file mode 100644 index 0000000000..8f1bcc7496 --- /dev/null +++ b/feign-core/src/main/java/feign/RetryableException.java @@ -0,0 +1,48 @@ +package feign; + +import com.google.common.base.Optional; +import com.google.common.net.HttpHeaders; + +import java.util.Date; + +import feign.codec.ErrorDecoder; + +/** + * This exception is raised when the {@link Response} is deemed to be retryable, + * typically via an {@link ErrorDecoder} when the {@link Response#status() + * status} is 503. + */ +public class RetryableException extends FeignException { + + private static final long serialVersionUID = 1L; + + private final Optional retryAfter; + + /** + * @param retryAfter usually corresponds to the {@link HttpHeaders#RETRY_AFTER} + * header. + */ + public RetryableException(String message, Throwable cause, Date retryAfter) { + super(message, cause); + this.retryAfter = Optional.fromNullable(retryAfter); + } + + /** + * @param retryAfter usually corresponds to the {@link HttpHeaders#RETRY_AFTER} + * header. + */ + public RetryableException(String message, Date retryAfter) { + super(message); + this.retryAfter = Optional.fromNullable(retryAfter); + } + + /** + * Sometimes corresponds to the {@link HttpHeaders#RETRY_AFTER} header + * present in {@code 503} status. Other times parsed from an + * application-specific response. + */ + public Optional retryAfter() { + return retryAfter; + } + +} diff --git a/feign-core/src/main/java/feign/Retryer.java b/feign-core/src/main/java/feign/Retryer.java new file mode 100644 index 0000000000..697f06c1d7 --- /dev/null +++ b/feign-core/src/main/java/feign/Retryer.java @@ -0,0 +1,66 @@ +package feign; + +import com.google.common.base.Ticker; + +import static com.google.common.primitives.Longs.max; +import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; + +/** + * Created for each invocation to {@link Client#execute(Request, feign.Request.Options)}. + * Implementations may keep state to determine if retry operations should + * continue or not. + */ +public interface Retryer { + + /** + * if retry is permitted, return (possibly after sleeping). Otherwise + * propagate the exception. + */ + void continueOrPropagate(RetryableException e); + + public static class Default implements Retryer { + private final int maxAttempts = 5; + private final long period = MILLISECONDS.toNanos(50); + private final long maxPeriod = SECONDS.toNanos(1); + + // visible for testing; + Ticker ticker = Ticker.systemTicker(); + int attempt; + long sleptForNanos; + + public Default() { + this.attempt = 1; + } + + public void continueOrPropagate(RetryableException e) { + if (attempt++ >= maxAttempts) + throw e; + + long interval; + if (e.retryAfter().isPresent()) { + interval = max(maxPeriod, e.retryAfter().get().getTime() - ticker.read(), 0); + } else { + interval = nextMaxInterval(); + } + sleepUninterruptibly(interval, NANOSECONDS); + sleptForNanos += interval; + } + + /** + * Calculates the time interval to a retry attempt. + *

+ * The interval increases exponentially with each attempt, at a rate of + * nextInterval *= 1.5 (where 1.5 is the backoff factor), to the maximum + * interval. + * + * @return time in nanoseconds from now until the next attempt. + */ + long nextMaxInterval() { + long interval = (long) (period * Math.pow(1.5, attempt - 1)); + return interval > maxPeriod ? maxPeriod : interval; + } + } +} diff --git a/feign-core/src/main/java/feign/Target.java b/feign-core/src/main/java/feign/Target.java new file mode 100644 index 0000000000..bd2724cb2c --- /dev/null +++ b/feign-core/src/main/java/feign/Target.java @@ -0,0 +1,98 @@ +package feign; + +import com.google.common.base.Function; +import com.google.common.base.Objects; +import com.google.common.base.Strings; + +import static com.google.common.base.Objects.equal; +import static com.google.common.base.Preconditions.checkNotNull; + +/** + *

relationship to JAXRS 2.0

+ *

+ * Similar to {@code javax.ws.rs.client.WebTarget}, as it produces requests. + * However, {@link RequestTemplate} is a closer match to {@code WebTarget}. + * + * @param type of the interface this target applies to. + */ +public interface Target extends Function { + /* The type of the interface this target applies to. ex. {@code Route53}. */ + Class type(); + + /* configuration key associated with this target. For example, {@code route53}. */ + String name(); + + /* base HTTP URL of the target. For example, {@code https://api/v2}. */ + String url(); + + /** + * Targets a template to this target, adding the {@link #url() base url} and + * any authentication headers. + *

+ *

+ * For example: + *

+ *

+   * public Request apply(RequestTemplate input) {
+   *     input.insert(0, url());
+   *     input.replaceHeader("X-Auth", currentToken);
+   *     return input.asRequest();
+   * }
+   * 
+ *

+ *

relationship to JAXRS 2.0

+ *

+ * This call is similar to {@code javax.ws.rs.client.WebTarget.request()}, + * except that we expect transient, but necessary decoration to be applied + * on invocation. + */ + @Override public Request apply(RequestTemplate input); + + public static class HardCodedTarget implements Target { + private final Class type; + private final String name; + private final String url; + + public HardCodedTarget(Class type, String url) { + this(type, url, url); + } + + public HardCodedTarget(Class type, String name, String url) { + this.type = checkNotNull(type, "type"); + this.name = checkNotNull(Strings.emptyToNull(name), "name"); + this.url = checkNotNull(Strings.emptyToNull(url), "url"); + } + + @Override public Class type() { + return type; + } + + @Override public String name() { + return name; + } + + @Override public String url() { + return url; + } + + /* no authentication or other special activity. just insert the url. */ + @Override public Request apply(RequestTemplate input) { + if (input.url().indexOf("http") != 0) + input.insert(0, url()); + return input.request(); + } + + @Override public int hashCode() { + return Objects.hashCode(type, name, url); + } + + @Override public boolean equals(Object obj) { + if (this == obj) + return true; + if (HardCodedTarget.class != obj.getClass()) + return false; + HardCodedTarget that = HardCodedTarget.class.cast(obj); + return equal(this.type, that.type) && equal(this.name, that.name) && equal(this.url, that.url); + } + } +} diff --git a/feign-core/src/main/java/feign/Wire.java b/feign-core/src/main/java/feign/Wire.java new file mode 100644 index 0000000000..d9cb08e28f --- /dev/null +++ b/feign-core/src/main/java/feign/Wire.java @@ -0,0 +1,139 @@ +package feign; + +import com.google.common.io.Closer; + +import java.io.BufferedReader; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Map.Entry; +import java.util.logging.FileHandler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; +import java.util.logging.SimpleFormatter; + +/** + * Writes http headers and body. Plumb to your favorite log impl. + */ +public abstract class Wire { + /** + * logs to the category {@link Wire} at {@link Level#FINE} + */ + public static class ErrorWire extends Wire { + final Logger logger = Logger.getLogger(Wire.class.getName()); + + @Override protected void log(Target target, String format, Object... args) { + System.err.printf(format + "%n", args); + } + } + + /** + * logs to the category {@link Wire} at {@link Level#FINE}, if loggable. + */ + public static class LoggingWire extends Wire { + final Logger logger = Logger.getLogger(Wire.class.getName()); + + @Override void wireRequest(Target target, Request request) { + if (logger.isLoggable(Level.FINE)) { + super.wireRequest(target, request); + } + } + + @Override Response wireAndRebufferResponse(Target target, Response response) throws IOException { + if (logger.isLoggable(Level.FINE)) { + return super.wireAndRebufferResponse(target, response); + } + return response; + } + + @Override protected void log(Target target, String format, Object... args) { + logger.fine(String.format(format, args)); + } + + /** + * helper that configures jul to sanely log messages. + */ + public LoggingWire appendToFile(String logfile) { + final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + logger.setLevel(Level.FINE); + try { + FileHandler handler = new FileHandler(logfile, true); + handler.setFormatter(new SimpleFormatter() { + @Override + public String format(LogRecord record) { + String timestamp = sdf.format(new java.util.Date(record.getMillis())); + return String.format("%s %s%n", timestamp, record.getMessage()); + } + }); + logger.addHandler(handler); + } catch (IOException e) { + throw new IllegalStateException("Could not add file handler.", e); + } + return this; + } + } + + public static class NoOpWire extends Wire { + @Override void wireRequest(Target target, Request request) { + } + + @Override Response wireAndRebufferResponse(Target target, Response response) throws IOException { + return response; + } + + @Override + protected void log(Target target, String format, Object... args) { + } + } + + /** + * 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} + */ + protected abstract void log(Target target, String format, Object... args); + + void wireRequest(Target target, Request request) { + log(target, ">> %s %s HTTP/1.1", request.method(), request.url()); + + for (Entry header : request.headers().entries()) { + log(target, ">> %s: %s", header.getKey(), header.getValue()); + } + + if (request.body().isPresent()) { + log(target, ">> "); // CRLF + log(target, ">> %s", request.body().get()); + } + } + + Response wireAndRebufferResponse(Target target, Response response) throws IOException { + log(target, "<< HTTP/1.1 %s %s", response.status(), response.reason()); + + for (Entry header : response.headers().entries()) { + log(target, "<< %s: %s", header.getKey(), header.getValue()); + } + + if (response.body().isPresent()) { + log(target, "<< "); // CRLF + Closer closer = Closer.create(); + try { + StringBuilder body = new StringBuilder(); + BufferedReader reader = new BufferedReader(closer.register(response.body().get().asReader())); + String line; + while ((line = reader.readLine()) != null) { + body.append(line); + log(target, "<< %s", line); + } + return Response.create(response.status(), response.reason(), response.headers(), body.toString()); + } catch (Throwable e) { + throw closer.rethrow(e); + } finally { + closer.close(); + } + } + return response; + } +} \ No newline at end of file diff --git a/feign-core/src/main/java/feign/codec/BodyEncoder.java b/feign-core/src/main/java/feign/codec/BodyEncoder.java new file mode 100644 index 0000000000..9f0b581de4 --- /dev/null +++ b/feign-core/src/main/java/feign/codec/BodyEncoder.java @@ -0,0 +1,41 @@ +package feign.codec; + +import feign.RequestTemplate; + +public interface BodyEncoder { + /** + * Converts objects to an appropriate representation. Can affect any part of + * {@link RequestTemplate}. + *

+ * Ex. + *

+ *

+   * public class GsonEncoder implements BodyEncoder {
+   *     private final Gson gson;
+   *
+   *     public GsonEncoder(Gson gson) {
+   *         this.gson = gson;
+   *     }
+   *
+   *     @Override
+   *     public void encodeBody(Object bodyParam, RequestTemplate base) {
+   *         base.body(gson.toJson(bodyParam));
+   *     }
+   *
+   * }
+   * 
+ *

+ * If a parameter has no {@code *Param} annotation, it is passed to this + * method. + *

+ *

+   * @POST
+   * @Path("/")
+   * void create(User user);
+   * 
+ * + * @param bodyParam a body parameter + * @param base template to encode the {@code object} into. + */ + void encodeBody(Object bodyParam, RequestTemplate base); +} diff --git a/feign-core/src/main/java/feign/codec/Decoder.java b/feign-core/src/main/java/feign/codec/Decoder.java new file mode 100644 index 0000000000..f35c2e4d76 --- /dev/null +++ b/feign-core/src/main/java/feign/codec/Decoder.java @@ -0,0 +1,84 @@ +package feign.codec; + +import com.google.common.io.Closer; +import com.google.common.reflect.TypeToken; + +import java.io.IOException; +import java.io.Reader; + +import feign.Response; + +/** + * Decodes an HTTP response into a given type. Invoked when + * {@link Response#status()} is in the 2xx range. + *

+ * Ex. + *

+ *

+ * public class GsonDecoder extends Decoder {
+ *     private final Gson gson;
+ *
+ *     public GsonDecoder(Gson gson) {
+ *         this.gson = gson;
+ *     }
+ *
+ *     @Override
+ *     public Object decode(String methodKey, Reader reader, TypeToken<?> type) {
+ *         return gson.fromJson(reader, type.getType());
+ *     }
+ * }
+ * 
+ *

+ *

Error handling

+ *

+ * Responses where {@link Response#status()} is not in the 2xx range are + * classified as errors, addressed by the {@link ErrorDecoder}. That said, + * certain RPC apis return errors defined in the {@link Response#body()} even on + * a 200 status. For example, in the DynECT api, a job still running condition + * is returned with a 200 status, encoded in json. When scenarios like this + * occur, you should raise an application-specific exception (which may be + * {@link feign.RetryableException retryable}). + */ +public abstract class Decoder { + + /** + * Override this method in order to consider the HTTP {@link Response} as + * opposed to just the {@link feign.Response.Body} when decoding into a new + * instance of {@code type}. + * + * @param methodKey {@link feign.Feign#configKey} of the java method that invoked the request. ex. {@code IAM#getUser()} + * @param response HTTP response. + * @param type Target object type. + * @return instance of {@code type} + * @throws IOException if there was a network error reading the response. + */ + public Object decode(String methodKey, Response response, TypeToken type) throws IOException { + Response.Body body = response.body().orNull(); + if (body == null) + return null; + Closer closer = Closer.create(); + try { + Reader reader = closer.register(body.asReader()); + return decode(methodKey, reader, type); + } catch (IOException e) { + throw closer.rethrow(e, IOException.class); + } catch (Throwable e) { + throw closer.rethrow(e); + } finally { + closer.close(); + } + } + + /** + * Implement this to decode a {@code Reader} to an object of the specified + * type. + * + * @param methodKey {@link feign.Feign#configKey} of the java method that invoked the request. ex. {@code IAM#getUser()} + * @param reader no need to close this, as {@link #decode(String, Response, TypeToken)} + * manages resources. + * @param type Target object type. + * @return instance of {@code type} + * @throws Throwable will be propagated safely to the caller. + */ + public abstract Object decode(String methodKey, Reader reader, TypeToken type) throws Throwable; +} \ No newline at end of file diff --git a/feign-core/src/main/java/feign/codec/Decoders.java b/feign-core/src/main/java/feign/codec/Decoders.java new file mode 100644 index 0000000000..12473e622a --- /dev/null +++ b/feign-core/src/main/java/feign/codec/Decoders.java @@ -0,0 +1,121 @@ +package feign.codec; + +import com.google.common.base.Function; +import com.google.common.base.Functions; +import com.google.common.collect.ImmutableList; +import com.google.common.io.CharStreams; +import com.google.common.reflect.TypeToken; + +import java.io.Reader; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.String.format; +import static java.util.regex.Pattern.DOTALL; +import static java.util.regex.Pattern.compile; + +/** + * Static utility methods pertaining to {@code Decoder} instances. + *

+ *

Pattern Decoders

+ *

+ * Pattern decoders typically require less initialization, dependencies, and + * code than reflective decoders, but not can be awkward to those unfamiliar + * with regex. Typical use of pattern decoders is to grab a single field from an + * xml response, or parse a list of Strings. The pattern decoders here + * facilitate these use cases. + */ +public class Decoders { + + /** + * The first match group is applied to {@code applyGroups} and result + * returned. If no matches are found, the response is null; + *

+ * Ex. to pull the first interesting element from an xml response: + *

+ *

+   * decodeFirstDirPoolID = transformFirstGroup("<DirPoolID[ˆ>]*>([ˆ<]+)</DirPoolID>", ToLong.INSTANCE);
+   * 
+ */ + public static Decoder transformFirstGroup(String pattern, final Function applyFirstGroup) { + final Pattern patternForMatcher = compile(checkNotNull(pattern, "pattern"), DOTALL); + checkNotNull(applyFirstGroup, "applyFirstGroup"); + return new Decoder() { + @Override public Object decode(String methodKey, Reader reader, TypeToken type) throws Throwable { + Matcher matcher = patternForMatcher.matcher(CharStreams.toString(reader)); + if (matcher.find()) { + return applyFirstGroup.apply(matcher.group(1)); + } + return null; + } + + @Override public String toString() { + return format("decode groups from %s into %s", patternForMatcher, applyFirstGroup); + } + }; + } + + /** + * shortcut for {@link Decoders#transformFirstGroup(String, Function)} when + * {@code String} is the type you are decoding into. + *

+ *

+ * Ex. to pull the first interesting element from an xml response: + *

+ *

+   * decodeFirstDirPoolID = firstGroup("<DirPoolID[ˆ>]*>([ˆ<]+)</DirPoolID>");
+   * 
+ */ + public static Decoder firstGroup(String pattern) { + return transformFirstGroup(pattern, Functions.identity()); + } + + /** + * On the each find the first match group is applied to + * {@code applyFirstGroup} and added to the list returned. If no matches are + * found, the response is an empty list; + *

+ * Ex. to pull a list zones constructed from http paths starting with + * {@code /Rest/Zone/}: + *

+ *

+   * decodeListOfZones = transformEachFirstGroup("/REST/Zone/([ˆ/]+)/", ToZone.INSTANCE);
+   * 
+ */ + public static Decoder transformEachFirstGroup(String pattern, final Function applyFirstGroup) { + final Pattern patternForMatcher = compile(checkNotNull(pattern, "pattern"), DOTALL); + checkNotNull(applyFirstGroup, "applyFirstGroup"); + return new Decoder() { + @Override public List decode(String methodKey, Reader reader, TypeToken type) throws Throwable { + Matcher matcher = patternForMatcher.matcher(CharStreams.toString(reader)); + ImmutableList.Builder builder = ImmutableList.builder(); + while (matcher.find()) { + builder.add(applyFirstGroup.apply(matcher.group(1))); + } + return builder.build(); + } + + @Override public String toString() { + return format("decode %s into list elements, where each group(1) is transformed with %s", + patternForMatcher, applyFirstGroup); + } + }; + } + + /** + * shortcut for {@link Decoders#transformEachFirstGroup(String, Function)} + * when {@code List} is the type you are decoding into. + *

+ * Ex. to pull a list zones names, which are http paths starting with + * {@code /Rest/Zone/}: + *

+ *

+   * decodeListOfZonesNames = eachFirstGroup("/REST/Zone/([ˆ/]+)/");
+   * 
+ */ + public static Decoder eachFirstGroup(String pattern) { + return transformEachFirstGroup(pattern, Functions.identity()); + } +} \ No newline at end of file diff --git a/feign-core/src/main/java/feign/codec/ErrorDecoder.java b/feign-core/src/main/java/feign/codec/ErrorDecoder.java new file mode 100644 index 0000000000..7651d3ec50 --- /dev/null +++ b/feign-core/src/main/java/feign/codec/ErrorDecoder.java @@ -0,0 +1,130 @@ +package feign.codec; + +import com.google.common.base.Function; +import com.google.common.base.Optional; +import com.google.common.base.Ticker; +import com.google.common.net.HttpHeaders; +import com.google.common.reflect.TypeToken; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import feign.FeignException; +import feign.Response; +import feign.RetryableException; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.Iterables.getFirst; +import static com.google.common.net.HttpHeaders.RETRY_AFTER; +import static feign.FeignException.errorStatus; +import static java.util.Locale.US; +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; + +/** + * Allows you to massage an exception into a application-specific one, or + * fallback to a default value. Falling back to null on + * {@link Response#status() status 404}, or converting out to a throttle + * exception are examples of this in use. + *

+ * Ex. + *

+ *

+ * class IllegalArgumentExceptionOn404Decoder extends ErrorDecoder {
+ *
+ *     @Override
+ *     public Object decode(String methodKey, Response response, TypeToken<?> type) throws Throwable {
+ *         if (response.status() == 404)
+ *             throw new IllegalArgumentException("zone not found");
+ *         return ErrorDecoder.DEFAULT.decode(request, response, type);
+ *     }
+ *
+ * }
+ * 
+ */ +public interface ErrorDecoder { + + /** + * Implement this method in order to decode an HTTP {@link Response} when + * {@link Response#status()} is not in the 2xx range. Please raise + * application-specific exceptions or return fallback values where possible. + * If your exception is retryable, wrap or subclass + * {@link RetryableException} + * + * @param methodKey {@link feign.Feign#configKey} of the java method that invoked the request. ex. {@code IAM#getUser()} + * @param response HTTP response where {@link Response#status() status} >= + * {@code 300}. + * @param type Target object type. + * @return instance of {@code type} + * @throws Throwable IOException, if there was a network error reading the + * response or an application-specific exception decoded by the + * implementation. If the throwable is retryable, it should be + * wrapped, or a subtype of {@link RetryableException} + */ + public Object decode(String methodKey, Response response, TypeToken type) throws Throwable; + + public static final ErrorDecoder DEFAULT = new ErrorDecoder() { + + private final RetryAfterDecoder retryAfterDecoder = new RetryAfterDecoder(); + + @Override + public Object decode(String methodKey, Response response, TypeToken type) throws Throwable { + FeignException exception = errorStatus(methodKey, response); + Optional retryAfter = retryAfterDecoder.apply(getFirst(response.headers().get(RETRY_AFTER), null)); + if (retryAfter.isPresent()) + throw new RetryableException(exception.getMessage(), exception, retryAfter.get()); + throw exception; + } + }; + + /** + * Decodes a {@link HttpHeaders#RETRY_AFTER} header into an absolute date, + * if possible. + * + * @see
Retry-After + * format + */ + static class RetryAfterDecoder implements Function> { + static final DateFormat RFC822_FORMAT = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", US); + private final Ticker currentTimeNanos; + private final DateFormat rfc822Format; + + RetryAfterDecoder() { + this(Ticker.systemTicker(), RFC822_FORMAT); + } + + RetryAfterDecoder(Ticker currentTimeNanos, DateFormat rfc822Format) { + this.currentTimeNanos = checkNotNull(currentTimeNanos, "currentTimeNanos"); + this.rfc822Format = checkNotNull(rfc822Format, "rfc822Format"); + } + + /** + * returns a date that corresponds to the first time a request can be + * retried. + * + * @param retryAfter String in Retry-After format + */ + @Override + public Optional apply(String retryAfter) { + if (retryAfter == null) + return Optional.absent(); + if (retryAfter.matches("^[0-9]+$")) { + long currentTimeMillis = NANOSECONDS.toMillis(currentTimeNanos.read()); + long deltaMillis = SECONDS.toMillis(Long.parseLong(retryAfter)); + return Optional.of(new Date(currentTimeMillis + deltaMillis)); + } + synchronized (rfc822Format) { + try { + return Optional.of(rfc822Format.parse(retryAfter)); + } catch (ParseException ignored) { + return Optional.absent(); + } + } + } + } +} \ No newline at end of file diff --git a/feign-core/src/main/java/feign/codec/FormEncoder.java b/feign-core/src/main/java/feign/codec/FormEncoder.java new file mode 100644 index 0000000000..a03d68e78d --- /dev/null +++ b/feign-core/src/main/java/feign/codec/FormEncoder.java @@ -0,0 +1,27 @@ +package feign.codec; + +import java.util.Map; + +import javax.ws.rs.FormParam; + +import feign.RequestTemplate; + +public interface FormEncoder { + + /** + * FormParam encoding + *

+ * If any parameters are annotated with {@link FormParam}, they will be + * collected and passed as {code formParams} + *

+ *

+   * @POST
+   * @Path("/")
+   * Session login(@FormParam("username") String username, @FormParam("password") String password);
+   * 
+ * + * @param formParams Object instance to convert. + * @param base template to encode the {@code object} into. + */ + void encodeForm(Map formParams, RequestTemplate base); +} \ No newline at end of file diff --git a/feign-core/src/main/java/feign/codec/SAXDecoder.java b/feign-core/src/main/java/feign/codec/SAXDecoder.java new file mode 100644 index 0000000000..92b861b8ab --- /dev/null +++ b/feign-core/src/main/java/feign/codec/SAXDecoder.java @@ -0,0 +1,50 @@ +package feign.codec; + +import com.google.common.reflect.TypeToken; + +import org.xml.sax.ContentHandler; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; + +import java.io.IOException; +import java.io.Reader; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParserFactory; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public abstract class SAXDecoder extends Decoder { + /* Implementations are not intended to be shared across requests. */ + public interface ContentHandlerWithResult extends ContentHandler { + /* expected to be set following a call to {@link XMLReader#parse(InputSource)} */ + Object getResult(); + } + + private final SAXParserFactory factory; + + protected SAXDecoder() { + this(SAXParserFactory.newInstance()); + factory.setNamespaceAware(false); + factory.setValidating(false); + } + + protected SAXDecoder(SAXParserFactory factory) { + this.factory = checkNotNull(factory, "factory"); + } + + @Override public Object decode(String methodKey, Reader reader, TypeToken type) throws IOException, SAXException, + ParserConfigurationException { + ContentHandlerWithResult handler = typeToNewHandler(type); + checkState(handler != null, "%s returned null for type %s", this, type); + XMLReader xmlReader = factory.newSAXParser().getXMLReader(); + xmlReader.setContentHandler(handler); + InputSource source = new InputSource(reader); + xmlReader.parse(source); + return handler.getResult(); + } + + protected abstract ContentHandlerWithResult typeToNewHandler(TypeToken type); +} diff --git a/feign-core/src/main/java/feign/codec/ToStringDecoder.java b/feign-core/src/main/java/feign/codec/ToStringDecoder.java new file mode 100644 index 0000000000..087143577b --- /dev/null +++ b/feign-core/src/main/java/feign/codec/ToStringDecoder.java @@ -0,0 +1,12 @@ +package feign.codec; + +import com.google.common.io.CharStreams; +import com.google.common.reflect.TypeToken; + +import java.io.Reader; + +public class ToStringDecoder extends Decoder { + @Override public Object decode(String methodKey, Reader reader, TypeToken type) throws Throwable { + return CharStreams.toString(reader); + } +} \ No newline at end of file diff --git a/feign-core/src/test/java/feign/ContractTest.java b/feign-core/src/test/java/feign/ContractTest.java new file mode 100644 index 0000000000..e3c96137e2 --- /dev/null +++ b/feign-core/src/test/java/feign/ContractTest.java @@ -0,0 +1,126 @@ +package feign; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +import org.testng.annotations.Test; + +import java.net.URI; + +import javax.ws.rs.DELETE; +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; + +import feign.RequestTemplate.Body; + +import static com.google.common.net.HttpHeaders.CONTENT_TYPE; +import static feign.Contract.parseAndValidatateMetadata; +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_XML; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +@Test +public class ContractTest { + + static interface Methods { + @POST void post(); + + @PUT void put(); + + @GET void get(); + + @DELETE void delete(); + } + + @Test public void httpMethods() throws Exception { + assertEquals(parseAndValidatateMetadata(Methods.class.getDeclaredMethod("post")).template().method(), POST); + assertEquals(parseAndValidatateMetadata(Methods.class.getDeclaredMethod("put")).template().method(), PUT); + assertEquals(parseAndValidatateMetadata(Methods.class.getDeclaredMethod("get")).template().method(), GET); + assertEquals(parseAndValidatateMetadata(Methods.class.getDeclaredMethod("delete")).template().method(), DELETE); + } + + static interface WithQueryParamsInPath { + @GET @Path("/?Action=GetUser&Version=2010-05-08") Response get(); + } + + @Test public void queryParamsInPathExtract() throws Exception { + MethodMetadata md = parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("get")); + assertEquals(md.template().url(), "/"); + assertEquals(md.template().queries().get("Action"), ImmutableSet.of("GetUser")); + assertEquals(md.template().queries().get("Version"), ImmutableSet.of("2010-05-08")); + } + + static interface BodyWithoutParameters { + @POST @Produces(APPLICATION_XML) @Body("") Response post(); + } + + @Test public void bodyWithoutParameters() throws Exception { + MethodMetadata md = parseAndValidatateMetadata(BodyWithoutParameters.class.getDeclaredMethod("post")); + assertEquals(md.template().body().get(), ""); + assertFalse(md.template().bodyTemplate().isPresent()); + assertTrue(md.formParams().isEmpty()); + assertTrue(md.indexToName().isEmpty()); + } + + @Test public void producesAddsContentTypeHeader() throws Exception { + MethodMetadata md = parseAndValidatateMetadata(BodyWithoutParameters.class.getDeclaredMethod("post")); + assertEquals(md.template().headers().get(CONTENT_TYPE), ImmutableSet.of(APPLICATION_XML)); + } + + static interface WithURIParam { + @GET @Path("/{1}/{2}") Response uriParam(@PathParam("1") String one, URI endpoint, @PathParam("2") String two); + } + + @Test public void methodCanHaveUriParam() throws Exception { + MethodMetadata md = parseAndValidatateMetadata(WithURIParam.class.getDeclaredMethod("uriParam", String.class, + URI.class, String.class)); + assertEquals(md.urlIndex(), Integer.valueOf(1)); + } + + @Test public void pathParamsParseIntoIndexToName() throws Exception { + MethodMetadata md = parseAndValidatateMetadata(WithURIParam.class.getDeclaredMethod("uriParam", String.class, + URI.class, String.class)); + assertEquals(md.template().url(), "/{1}/{2}"); + assertEquals(md.indexToName().get(0), ImmutableSet.of("1")); + assertEquals(md.indexToName().get(2), ImmutableSet.of("2")); + } + + static interface FormParams { + @POST @Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D") void login(@FormParam("customer_name") String customer, @FormParam("user_name") String user); + } + + @Test public void formParamsParseIntoIndexToName() throws Exception { + MethodMetadata md = parseAndValidatateMetadata(FormParams.class.getDeclaredMethod("login", String.class, + String.class, String.class)); + + assertFalse(md.template().body().isPresent()); + assertEquals(md.template().bodyTemplate().get(), + "%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D"); + 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")); + } + + static interface HeaderParams { + @POST void logout(@HeaderParam("Auth-Token") String token); + } + + @Test public void headerParamsParseIntoIndexToName() throws Exception { + MethodMetadata md = parseAndValidatateMetadata(HeaderParams.class.getDeclaredMethod("logout", String.class)); + + assertEquals(md.template().headers().get("Auth-Token"), ImmutableSet.of("{Auth-Token}")); + assertEquals(md.indexToName().get(0), ImmutableSet.of("Auth-Token")); + } +} diff --git a/feign-core/src/test/java/feign/DefaultRetryerTest.java b/feign-core/src/test/java/feign/DefaultRetryerTest.java new file mode 100644 index 0000000000..337125d88b --- /dev/null +++ b/feign-core/src/test/java/feign/DefaultRetryerTest.java @@ -0,0 +1,59 @@ +package feign; + +import com.google.common.base.Ticker; + +import org.testng.annotations.Test; + +import java.util.Date; + +import feign.Retryer.Default; + +import static org.testng.Assert.assertEquals; + +@Test +public class DefaultRetryerTest { + + @Test(expectedExceptions = RetryableException.class) + public void only5TriesAllowedAndExponentialBackoff() throws Exception { + RetryableException e = new RetryableException(null, null, null); + Default retryer = new Retryer.Default(); + assertEquals(retryer.attempt, 1); + assertEquals(retryer.sleptForNanos, 0); + + retryer.continueOrPropagate(e); + assertEquals(retryer.attempt, 2); + assertEquals(retryer.sleptForNanos, 75000000); + + retryer.continueOrPropagate(e); + assertEquals(retryer.attempt, 3); + assertEquals(retryer.sleptForNanos, 187500000); + + retryer.continueOrPropagate(e); + assertEquals(retryer.attempt, 4); + assertEquals(retryer.sleptForNanos, 356250000); + + retryer.continueOrPropagate(e); + assertEquals(retryer.attempt, 5); + assertEquals(retryer.sleptForNanos, 609375000); + + retryer.continueOrPropagate(e); + // fail + } + + @Test public void considersRetryAfterButNotMoreThanMaxPeriod() throws Exception { + Default retryer = new Retryer.Default(); + retryer.ticker = epoch; + + retryer.continueOrPropagate(new RetryableException(null, null, new Date(5000))); + assertEquals(retryer.attempt, 2); + assertEquals(retryer.sleptForNanos, 1000000000); + } + + static Ticker epoch = new Ticker() { + @Override + public long read() { + return 0; + } + }; + +} diff --git a/feign-core/src/test/java/feign/FeignTest.java b/feign-core/src/test/java/feign/FeignTest.java new file mode 100644 index 0000000000..7f2df044a0 --- /dev/null +++ b/feign-core/src/test/java/feign/FeignTest.java @@ -0,0 +1,168 @@ +package feign; + +import com.google.common.collect.ImmutableMap; +import com.google.common.reflect.TypeToken; +import com.google.mockwebserver.MockResponse; +import com.google.mockwebserver.MockWebServer; +import com.google.mockwebserver.SocketPolicy; + +import org.testng.annotations.Test; + +import java.io.IOException; +import java.io.Reader; +import java.net.URI; +import java.util.Map; + +import javax.inject.Singleton; +import javax.net.ssl.SSLSocketFactory; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; + +import dagger.Module; +import dagger.Provides; +import feign.codec.Decoder; +import feign.codec.ErrorDecoder; +import feign.codec.ToStringDecoder; + +import static org.testng.Assert.assertEquals; + +@Test +public class FeignTest { + static interface TestInterface { + @POST String post(); + + @GET @Path("/{1}/{2}") Response uriParam(@PathParam("1") String one, URI endpoint, @PathParam("2") String two); + + @dagger.Module(overrides = true, library = true) + static class Module { + // until dagger supports real map binding, we need to recreate the + // entire map, as opposed to overriding a single entry. + @Provides @Singleton Map decoders() { + return ImmutableMap.of("TestInterface", new ToStringDecoder()); + } + } + } + + @Test public void toKeyMethodFormatsAsExpected() throws Exception { + assertEquals(Feign.configKey(TestInterface.class.getDeclaredMethod("post")), "TestInterface#post()"); + assertEquals(Feign.configKey(TestInterface.class.getDeclaredMethod("uriParam", String.class, URI.class, + String.class)), "TestInterface#uriParam(String,URI,String)"); + } + + @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "zone not found") + public void canOverrideErrorDecoderOnMethod() throws IOException, InterruptedException { + @dagger.Module(overrides = true, includes = TestInterface.Module.class) class Overrides { + @Provides @Singleton Map decoders() { + return ImmutableMap.of("TestInterface#post()", new ErrorDecoder() { + + @Override + public Object decode(String methodKey, Response response, TypeToken type) throws Throwable { + if (response.status() == 404) + throw new IllegalArgumentException("zone not found"); + return ErrorDecoder.DEFAULT.decode(methodKey, response, type); + } + + }); + } + } + + 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()); + + api.post(); + } finally { + server.shutdown(); + } + } + + @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.play(); + + try { + TestInterface api = Feign.create(TestInterface.class, server.getUrl("").toString(), + new TestInterface.Module()); + + api.post(); + assertEquals(server.getRequestCount(), 2); + + } finally { + server.shutdown(); + } + } + + @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.play(); + + try { + @dagger.Module(overrides = true) class Overrides { + @Provides @Singleton Map decoders() { + return ImmutableMap.of("TestInterface", new Decoder() { + + @Override + public Object decode(String methodKey, Reader reader, TypeToken type) throws Throwable { + throw new IOException("error reading response"); + } + + }); + } + } + TestInterface api = Feign.create(TestInterface.class, server.getUrl("").toString(), new Overrides()); + + api.post(); + } finally { + server.shutdown(); + assertEquals(server.getRequestCount(), 1); + } + } + + @Module(injects = Client.Default.class, overrides = true) + static class TrustSSLSockets { + @Provides SSLSocketFactory trustingSSLSocketFactory() { + return TrustingSSLSocketFactory.get(); + } + } + + @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.play(); + + try { + TestInterface api = Feign.create(TestInterface.class, server.getUrl("").toString(), + new TestInterface.Module(), new TrustSSLSockets()); + api.post(); + } finally { + server.shutdown(); + } + } + + @Test public void retriesFailedHandshake() throws IOException, InterruptedException { + MockWebServer server = new MockWebServer(); + server.useHttps(TrustingSSLSocketFactory.get(), false); + server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE)); + server.enqueue(new MockResponse().setResponseCode(200).setBody("success!".getBytes())); + server.play(); + + try { + TestInterface api = Feign.create(TestInterface.class, server.getUrl("").toString(), + new TestInterface.Module(), new TrustSSLSockets()); + api.post(); + assertEquals(server.getRequestCount(), 2); + } finally { + server.shutdown(); + } + } +} diff --git a/feign-core/src/test/java/feign/RequestTemplateTest.java b/feign-core/src/test/java/feign/RequestTemplateTest.java new file mode 100644 index 0000000000..881fb08854 --- /dev/null +++ b/feign-core/src/test/java/feign/RequestTemplateTest.java @@ -0,0 +1,72 @@ +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; +import static javax.ws.rs.HttpMethod.GET; +import static org.testng.Assert.assertEquals; + +public class RequestTemplateTest { + @Test public void expandNotUrlEncoded() { + for (String val : ImmutableList.of("apples", "sp ace", "unic???de", "qu?stion")) + assertEquals(expand("/users/{user}", ImmutableMap.of("user", val)), "/users/" + val); + } + + @Test public void expandMultipleParams() { + assertEquals(expand("/users/{user}/{repo}", ImmutableMap.of("user", "unic???de", "repo", "foo")), + "/users/unic???de/foo"); + } + + @Test public void expandParamKeyHyphen() { + assertEquals(expand("/{user-dir}", ImmutableMap.of("user-dir", "foo")), "/foo"); + } + + @Test public void expandMissingParamProceeds() { + assertEquals(expand("/{user-dir}", ImmutableMap.of("user_dir", "foo")), "/{user-dir}"); + } + + @Test public void resolveTemplateWithParameterizedPathSkipsEncodingSlash() { + + RequestTemplate template = new RequestTemplate().method(GET) + .append("{zoneId}"); + + assertEquals(template.toString(), ""// + + "GET {zoneId} HTTP/1.1\n"); + + template.resolve(ImmutableMap.of("zoneId", "/hostedzone/Z1PA6795UKMFR9")); + + assertEquals(template.toString(), ""// + + "GET /hostedzone/Z1PA6795UKMFR9 HTTP/1.1\n"); + + template.insert(0, "https://route53.amazonaws.com/2012-12-12"); + + assertEquals(template.request().toString(), ""// + + "GET https://route53.amazonaws.com/2012-12-12/hostedzone/Z1PA6795UKMFR9 HTTP/1.1\n"); + } + + @Test public void resolveTemplateWithBaseAndParameterizedQuery() { + RequestTemplate template = new RequestTemplate().method(GET) + .append("/?Action=DescribeRegions").query("RegionName.1", "{region}"); + + assertEquals(template.queries(), + ImmutableListMultimap.of("Action", "DescribeRegions", "RegionName.1", "{region}")); + assertEquals(template.toString(), ""// + + "GET /?Action=DescribeRegions&RegionName.1={region} HTTP/1.1\n"); + + template.resolve(ImmutableMap.of("region", "eu-west-1")); + assertEquals(template.queries(), + ImmutableListMultimap.of("Action", "DescribeRegions", "RegionName.1", "eu-west-1")); + + assertEquals(template.toString(), ""// + + "GET /?Action=DescribeRegions&RegionName.1=eu-west-1 HTTP/1.1\n"); + + template.insert(0, "https://iam.amazonaws.com"); + + assertEquals(template.request().toString(), ""// + + "GET https://iam.amazonaws.com/?Action=DescribeRegions&RegionName.1=eu-west-1 HTTP/1.1\n"); + } +} diff --git a/feign-core/src/test/java/feign/TrustingSSLSocketFactory.java b/feign-core/src/test/java/feign/TrustingSSLSocketFactory.java new file mode 100644 index 0000000000..29ae665629 --- /dev/null +++ b/feign-core/src/test/java/feign/TrustingSSLSocketFactory.java @@ -0,0 +1,99 @@ +package feign; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; +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, UnknownHostException { + 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, + UnknownHostException { + 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) { + return; + } + + public void checkServerTrusted(X509Certificate[] certs, String authType) { + return; + } + + private final static String[] ENABLED_CIPHER_SUITES = {"SSL_DH_anon_WITH_RC4_128_MD5"}; + + private static enum Singleton implements Provider { + INSTANCE; + + private final SSLSocketFactory sslSocketFactory = new TrustingSSLSocketFactory(); + + @Override public SSLSocketFactory get() { + return sslSocketFactory; + } + } +} diff --git a/feign-core/src/test/java/feign/codec/DefaultErrorDecoderTest.java b/feign-core/src/test/java/feign/codec/DefaultErrorDecoderTest.java new file mode 100644 index 0000000000..502764560f --- /dev/null +++ b/feign-core/src/test/java/feign/codec/DefaultErrorDecoderTest.java @@ -0,0 +1,38 @@ +package feign.codec; + +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.reflect.TypeToken; + +import org.testng.annotations.Test; + +import feign.FeignException; +import feign.Response; +import feign.RetryableException; + +import static com.google.common.net.HttpHeaders.RETRY_AFTER; + +public class DefaultErrorDecoderTest { + @Test(expectedExceptions = FeignException.class, expectedExceptionsMessageRegExp = "status 500 reading Service#foo\\(\\)") + public void throwsFeignException() throws Throwable { + Response response = Response.create(500, "Internal server error", ImmutableListMultimap.of(), + null); + + ErrorDecoder.DEFAULT.decode("Service#foo()", response, TypeToken.of(Void.class)); + } + + @Test(expectedExceptions = FeignException.class, expectedExceptionsMessageRegExp = "status 500 reading Service#foo\\(\\); content:\nhello world") + public void throwsFeignExceptionIncludingBody() throws Throwable { + Response response = Response.create(500, "Internal server error", ImmutableListMultimap.of(), + "hello world"); + + ErrorDecoder.DEFAULT.decode("Service#foo()", response, TypeToken.of(Void.class)); + } + + @Test(expectedExceptions = RetryableException.class, expectedExceptionsMessageRegExp = "status 503 reading Service#foo\\(\\)") + public void retryAfterHeaderThrowsRetryableException() throws Throwable { + Response response = Response.create(503, "Service Unavailable", + ImmutableListMultimap.of(RETRY_AFTER, "Sat, 1 Jan 2000 00:00:00 GMT"), null); + + ErrorDecoder.DEFAULT.decode("Service#foo()", response, TypeToken.of(Void.class)); + } +} diff --git a/feign-core/src/test/java/feign/codec/RetryAfterDecoderTest.java b/feign-core/src/test/java/feign/codec/RetryAfterDecoderTest.java new file mode 100644 index 0000000000..a9be5b2df3 --- /dev/null +++ b/feign-core/src/test/java/feign/codec/RetryAfterDecoderTest.java @@ -0,0 +1,46 @@ +package feign.codec; + +import com.google.common.base.Ticker; + +import org.testng.annotations.Test; + +import java.text.ParseException; + +import feign.codec.ErrorDecoder.RetryAfterDecoder; + +import static feign.codec.ErrorDecoder.RetryAfterDecoder.RFC822_FORMAT; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; + +public class RetryAfterDecoderTest { + + @Test public void malformDateFailsGracefully() { + assertFalse(decoder.apply("Fri, 31 Dec 1999 23:59:59 ZBW").isPresent()); + } + + @Test public void rfc822Parses() throws ParseException { + assertEquals(decoder.apply("Fri, 31 Dec 1999 23:59:59 GMT").get(), + RFC822_FORMAT.parse("Fri, 31 Dec 1999 23:59:59 GMT")); + } + + @Test public void relativeSecondsParses() throws ParseException { + assertEquals(decoder.apply("86400").get(), RFC822_FORMAT.parse("Sun, 2 Jan 2000 00:00:00 GMT")); + } + + static Ticker y2k = new Ticker() { + + @Override + public long read() { + try { + return MILLISECONDS.toNanos(RFC822_FORMAT.parse("Sat, 1 Jan 2000 00:00:00 GMT").getTime()); + } catch (ParseException e) { + throw new RuntimeException(e); + } + } + + }; + + private RetryAfterDecoder decoder = new RetryAfterDecoder(y2k, RFC822_FORMAT); + +} diff --git a/feign-core/src/test/java/feign/examples/GitHubExample.java b/feign-core/src/test/java/feign/examples/GitHubExample.java new file mode 100644 index 0000000000..76d813e687 --- /dev/null +++ b/feign-core/src/test/java/feign/examples/GitHubExample.java @@ -0,0 +1,93 @@ +package feign.examples; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableMap; +import com.google.common.reflect.TypeToken; +import com.google.gson.Gson; + +import java.io.IOException; +import java.io.Reader; +import java.util.List; +import java.util.Map; + +import javax.inject.Singleton; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; + +import dagger.Module; +import dagger.Provides; +import feign.Feign; +import feign.codec.Decoder; +import feign.codec.Decoders; + +import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY; +import static com.fasterxml.jackson.annotation.PropertyAccessor.FIELD; +import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; + +/** + * adapted from {@code com.example.retrofit.GitHubClient} + */ +public class GitHubExample { + + interface GitHub { + @GET @Path("/repos/{owner}/{repo}/contributors") List contributors(@PathParam("owner") String owner, @PathParam("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 contributors = github.contributors("netflix", "feign"); + for (Contributor contributor : contributors) { + System.out.println(contributor.login + " (" + contributor.contributions + ")"); + } + } + + /** + * Here's how to wire gson deserialization. + * + * @see Decoders + */ + @Module(overrides = true, library = true) + static class GsonModule { + @Provides @Singleton Map decoders() { + return ImmutableMap.of("GitHub", jsonDecoder); + } + + final Decoder jsonDecoder = new Decoder() { + Gson gson = new Gson(); + + @Override public Object decode(String methodKey, Reader reader, TypeToken type) { + return gson.fromJson(reader, type.getType()); + } + }; + } + + /** + * Here's how to wire jackson deserialization. + * + * @see Decoders + */ + @Module(overrides = true, library = true) + static class JacksonModule { + @Provides @Singleton Map decoders() { + return ImmutableMap.of("GitHub", jsonDecoder); + } + + final Decoder jsonDecoder = new Decoder() { + ObjectMapper mapper = new ObjectMapper().disable(FAIL_ON_UNKNOWN_PROPERTIES).setVisibility(FIELD, ANY); + + @Override public Object decode(String methodKey, Reader reader, final TypeToken type) + throws JsonProcessingException, IOException { + return mapper.readValue(reader, mapper.constructType(type.getType())); + } + }; + } +} diff --git a/feign-core/src/test/java/feign/examples/IAMExample.java b/feign-core/src/test/java/feign/examples/IAMExample.java new file mode 100644 index 0000000000..c63faf2caa --- /dev/null +++ b/feign-core/src/test/java/feign/examples/IAMExample.java @@ -0,0 +1,201 @@ +package feign.examples; + +import com.google.common.base.Function; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Multimap; +import com.google.common.collect.TreeMultimap; + +import java.net.URI; +import java.text.SimpleDateFormat; +import java.util.Collection; +import java.util.Date; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TimeZone; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import javax.inject.Singleton; +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 static com.google.common.base.Charsets.UTF_8; +import static com.google.common.base.Throwables.propagate; +import static com.google.common.collect.Iterables.transform; +import static com.google.common.hash.Hashing.sha256; +import static com.google.common.io.BaseEncoding.base16; +import static com.google.common.net.HttpHeaders.HOST; + +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 { + + @Override public Class 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) + static class IAMModule { + @Provides @Singleton Map decoders() { + return ImmutableMap.of("IAM#arn()", Decoders.firstGroup("([\\S&&[^<]]+)")); + } + } + + // http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html + static class AWSSignatureVersion4 implements Function { + + String region = "us-east-1"; + String service = "iam"; + String accessKey; + String secretKey; + + public AWSSignatureVersion4(String accessKey, String secretKey) { + this.accessKey = accessKey; + this.secretKey = secretKey; + } + + @Override public Request apply(RequestTemplate input) { + input.header(HOST, URI.create(input.url()).getHost()); + Multimap sortedLowercaseHeaders = TreeMultimap.create(); + for (String key : input.headers().keySet()) { + sortedLowercaseHeaders.putAll(trimToLowercase.apply(key), + transform(input.headers().get(key), trimToLowercase)); + } + + String timestamp = iso8601.format(new Date()); + String credentialScope = Joiner.on('/').join(timestamp.substring(0, 8), region, service, "aws4_request"); + + input.query("X-Amz-Algorithm", "AWS4-HMAC-SHA256"); + input.query("X-Amz-Credential", accessKey + "/" + credentialScope); + input.query("X-Amz-Date", timestamp); + input.query("X-Amz-SignedHeaders", Joiner.on(';').join(sortedLowercaseHeaders.keySet())); + + String canonicalString = canonicalString(input, sortedLowercaseHeaders); + String toSign = toSign(timestamp, credentialScope, canonicalString); + + byte[] signatureKey = signatureKey(secretKey, timestamp); + String signature = base16().lowerCase().encode(hmacSHA256(toSign, signatureKey)); + + input.query("X-Amz-Signature", signature); + + return input.request(); + } + + byte[] signatureKey(String secretKey, String timestamp) { + byte[] kSecret = ("AWS4" + secretKey).getBytes(UTF_8); + byte[] kDate = hmacSHA256(timestamp.substring(0, 8), kSecret); + byte[] kRegion = hmacSHA256(region, kDate); + byte[] kService = hmacSHA256(service, kRegion); + byte[] kSigning = hmacSHA256("aws4_request", kService); + return kSigning; + } + + static byte[] hmacSHA256(String data, byte[] key) { + try { + String algorithm = "HmacSHA256"; + Mac mac = Mac.getInstance(algorithm); + mac.init(new SecretKeySpec(key, algorithm)); + return mac.doFinal(data.getBytes(UTF_8)); + } catch (Exception e) { + throw propagate(e); + } + } + + private static final String EMPTY_STRING_HASH = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + + private String canonicalString(RequestTemplate input, Multimap sortedLowercaseHeaders) { + StringBuilder canonicalRequest = new StringBuilder(); + // HTTPRequestMethod + '\n' + + canonicalRequest.append(input.method()).append('\n'); + + // CanonicalURI + '\n' + + canonicalRequest.append(URI.create(input.url()).getPath()).append('\n'); + + // CanonicalQueryString + '\n' + + canonicalRequest.append(input.queryLine().substring(1)); + canonicalRequest.append('\n'); + + // CanonicalHeaders + '\n' + + for (Entry> entry : sortedLowercaseHeaders.asMap().entrySet()) { + canonicalRequest.append(entry.getKey()).append(':').append(Joiner.on(',').join(entry.getValue())) + .append('\n'); + } + canonicalRequest.append('\n'); + + // SignedHeaders + '\n' + + canonicalRequest.append(Joiner.on(',').join(sortedLowercaseHeaders.keySet())).append('\n'); + + // HexEncode(Hash(Payload)) + if (input.body().isPresent()) { + canonicalRequest.append(base16().lowerCase().encode( + sha256().hashString(input.body().or(""), UTF_8).asBytes())); + } else { + canonicalRequest.append(EMPTY_STRING_HASH); + } + return canonicalRequest.toString(); + } + + private static final Function trimToLowercase = new Function() { + public String apply(String in) { + return in.toLowerCase().trim(); + } + }; + + private String toSign(String timestamp, String credentialScope, String canonicalRequest) { + StringBuilder toSign = new StringBuilder(); + // Algorithm + '\n' + + toSign.append("AWS4-HMAC-SHA256").append('\n'); + // RequestDate + '\n' + + toSign.append(timestamp).append('\n'); + // CredentialScope + '\n' + + toSign.append(credentialScope).append('\n'); + // HexEncode(Hash(CanonicalRequest)) + toSign.append(base16().lowerCase().encode(sha256().hashString(canonicalRequest, UTF_8).asBytes())); + return toSign.toString(); + } + + private static final SimpleDateFormat iso8601 = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"); + + static { + iso8601.setTimeZone(TimeZone.getTimeZone("GMT")); + } + } +} diff --git a/gradle.properties b/gradle.properties index 6b59bf6c43..8d0c7be962 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=1.4-SNAPSHOT +version=1.0.0-SNAPSHOT diff --git a/settings.gradle b/settings.gradle index 5dd25eb8c6..a98b5acfa4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ -rootProject.name='gradle-template-multi' // TEMPLATE: Change this -include 'template-client','template-server' +rootProject.name='feign' +include 'feign-core' From 53402b10dc1436ae12b0caff44a10b78aa42ad1d Mon Sep 17 00:00:00 2001 From: adriancole Date: Wed, 26 Jun 2013 18:28:48 -0700 Subject: [PATCH 045/179] bump --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 8d0c7be962..07ff68b985 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=1.0.0-SNAPSHOT +version=2.0.0-SNAPSHOT From aead1b73f721d1c5609f45d43e7700b21c34b133 Mon Sep 17 00:00:00 2001 From: adriancole Date: Thu, 27 Jun 2013 11:35:15 -0700 Subject: [PATCH 046/179] fixed excessively long lines and testng setup --- build.gradle | 8 ++++++-- feign-core/src/main/java/feign/ReflectiveFeign.java | 12 ++++++++---- feign-core/src/main/java/feign/Wire.java | 6 ++++-- feign-core/src/main/java/feign/codec/Decoders.java | 6 ++++-- feign-core/src/main/java/feign/codec/SAXDecoder.java | 3 ++- .../src/main/java/feign/codec/ToStringDecoder.java | 3 ++- feign-core/src/test/java/feign/ContractTest.java | 9 +++++++-- feign-core/src/test/java/feign/FeignTest.java | 3 ++- .../test/java/feign/TrustingSSLSocketFactory.java | 12 ++++++++---- .../src/test/java/feign/examples/GitHubExample.java | 3 ++- 10 files changed, 45 insertions(+), 20 deletions(-) diff --git a/build.gradle b/build.gradle index f99c728e51..2325ad8301 100644 --- a/build.gradle +++ b/build.gradle @@ -28,6 +28,10 @@ subprojects { project(':feign-core') { apply plugin: 'java' + test { + useTestNG() + } + dependencies { compile 'com.google.guava:guava:14.0.1' compile 'com.squareup.dagger:dagger:1.0.1' @@ -37,5 +41,5 @@ project(':feign-core') { testCompile 'com.fasterxml.jackson.core:jackson-databind:2.2.2' testCompile 'org.testng:testng:6.8.1' testCompile 'com.google.mockwebserver:mockwebserver:20130505' - } -} + } +} \ No newline at end of file diff --git a/feign-core/src/main/java/feign/ReflectiveFeign.java b/feign-core/src/main/java/feign/ReflectiveFeign.java index c65b885aca..a13e0c5201 100644 --- a/feign-core/src/main/java/feign/ReflectiveFeign.java +++ b/feign-core/src/main/java/feign/ReflectiveFeign.java @@ -66,7 +66,8 @@ static class FeignInvocationHandler extends AbstractInvocationHandler { this.methodToHandler = checkNotNull(methodToHandler, "methodToHandler for %s", target); } - @Override protected Object handleInvocation(Object proxy, Method method, Object[] args) throws Throwable { + @Override + protected Object handleInvocation(Object proxy, Method method, Object[] args) throws Throwable { return methodToHandler.get(method).invoke(args); } @@ -97,7 +98,8 @@ public static class Module { return in; } - @Provides Function> targetToHandlersByName(ParseHandlersByName parseHandlersByName) { + @Provides + Function> targetToHandlersByName(ParseHandlersByName parseHandlersByName) { return parseHandlersByName; } } @@ -208,7 +210,8 @@ private BuildFormEncodedTemplateFromArgs(MethodMetadata metadata, FormEncoder fo this.formEncoder = formEncoder; } - @Override protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, ImmutableMap variables) { + @Override + protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, ImmutableMap variables) { formEncoder.encodeForm(Maps.filterKeys(variables, Predicates.in(metadata.formParams())), mutable); return super.resolve(argv, mutable, variables); } @@ -222,7 +225,8 @@ private BuildBodyEncodedTemplateFromArgs(MethodMetadata metadata, BodyEncoder bo this.bodyEncoder = bodyEncoder; } - @Override protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, ImmutableMap variables) { + @Override + protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, ImmutableMap variables) { Object body = argv[metadata.bodyIndex()]; checkArgument(body != null, "Body parameter %s was null", metadata.bodyIndex()); bodyEncoder.encodeBody(body, mutable); diff --git a/feign-core/src/main/java/feign/Wire.java b/feign-core/src/main/java/feign/Wire.java index d9cb08e28f..76e17b929f 100644 --- a/feign-core/src/main/java/feign/Wire.java +++ b/feign-core/src/main/java/feign/Wire.java @@ -39,7 +39,8 @@ public static class LoggingWire extends Wire { } } - @Override Response wireAndRebufferResponse(Target target, Response response) throws IOException { + @Override + Response wireAndRebufferResponse(Target target, Response response) throws IOException { if (logger.isLoggable(Level.FINE)) { return super.wireAndRebufferResponse(target, response); } @@ -77,7 +78,8 @@ public static class NoOpWire extends Wire { @Override void wireRequest(Target target, Request request) { } - @Override Response wireAndRebufferResponse(Target target, Response response) throws IOException { + @Override + Response wireAndRebufferResponse(Target target, Response response) throws IOException { return response; } diff --git a/feign-core/src/main/java/feign/codec/Decoders.java b/feign-core/src/main/java/feign/codec/Decoders.java index 12473e622a..54a7973755 100644 --- a/feign-core/src/main/java/feign/codec/Decoders.java +++ b/feign-core/src/main/java/feign/codec/Decoders.java @@ -43,7 +43,8 @@ public static Decoder transformFirstGroup(String pattern, final Function type) throws Throwable { + @Override + public Object decode(String methodKey, Reader reader, TypeToken type) throws Throwable { Matcher matcher = patternForMatcher.matcher(CharStreams.toString(reader)); if (matcher.find()) { return applyFirstGroup.apply(matcher.group(1)); @@ -88,7 +89,8 @@ public static Decoder transformEachFirstGroup(String pattern, final Function final Pattern patternForMatcher = compile(checkNotNull(pattern, "pattern"), DOTALL); checkNotNull(applyFirstGroup, "applyFirstGroup"); return new Decoder() { - @Override public List decode(String methodKey, Reader reader, TypeToken type) throws Throwable { + @Override + public List decode(String methodKey, Reader reader, TypeToken type) throws Throwable { Matcher matcher = patternForMatcher.matcher(CharStreams.toString(reader)); ImmutableList.Builder builder = ImmutableList.builder(); while (matcher.find()) { diff --git a/feign-core/src/main/java/feign/codec/SAXDecoder.java b/feign-core/src/main/java/feign/codec/SAXDecoder.java index 92b861b8ab..1a0184c76d 100644 --- a/feign-core/src/main/java/feign/codec/SAXDecoder.java +++ b/feign-core/src/main/java/feign/codec/SAXDecoder.java @@ -35,7 +35,8 @@ protected SAXDecoder(SAXParserFactory factory) { this.factory = checkNotNull(factory, "factory"); } - @Override public Object decode(String methodKey, Reader reader, TypeToken type) throws IOException, SAXException, + @Override + public Object decode(String methodKey, Reader reader, TypeToken type) throws IOException, SAXException, ParserConfigurationException { ContentHandlerWithResult handler = typeToNewHandler(type); checkState(handler != null, "%s returned null for type %s", this, type); diff --git a/feign-core/src/main/java/feign/codec/ToStringDecoder.java b/feign-core/src/main/java/feign/codec/ToStringDecoder.java index 087143577b..1c4a7948cb 100644 --- a/feign-core/src/main/java/feign/codec/ToStringDecoder.java +++ b/feign-core/src/main/java/feign/codec/ToStringDecoder.java @@ -6,7 +6,8 @@ import java.io.Reader; public class ToStringDecoder extends Decoder { - @Override public Object decode(String methodKey, Reader reader, TypeToken type) throws Throwable { + @Override + public Object decode(String methodKey, Reader reader, TypeToken type) throws Throwable { return CharStreams.toString(reader); } } \ No newline at end of file diff --git a/feign-core/src/test/java/feign/ContractTest.java b/feign-core/src/test/java/feign/ContractTest.java index e3c96137e2..9e2b1ed0eb 100644 --- a/feign-core/src/test/java/feign/ContractTest.java +++ b/feign-core/src/test/java/feign/ContractTest.java @@ -79,7 +79,8 @@ static interface BodyWithoutParameters { } static interface WithURIParam { - @GET @Path("/{1}/{2}") Response uriParam(@PathParam("1") String one, URI endpoint, @PathParam("2") String two); + @GET @Path("/{1}/{2}") + Response uriParam(@PathParam("1") String one, URI endpoint, @PathParam("2") String two); } @Test public void methodCanHaveUriParam() throws Exception { @@ -97,7 +98,11 @@ static interface WithURIParam { } static interface FormParams { - @POST @Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D") void login(@FormParam("customer_name") String customer, @FormParam("user_name") String user); + @POST + @Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D") + void login( + @FormParam("customer_name") String customer, + @FormParam("user_name") String user, @FormParam("password") String password); } @Test public void formParamsParseIntoIndexToName() throws Exception { diff --git a/feign-core/src/test/java/feign/FeignTest.java b/feign-core/src/test/java/feign/FeignTest.java index 7f2df044a0..5db23bfe06 100644 --- a/feign-core/src/test/java/feign/FeignTest.java +++ b/feign-core/src/test/java/feign/FeignTest.java @@ -33,7 +33,8 @@ public class FeignTest { static interface TestInterface { @POST String post(); - @GET @Path("/{1}/{2}") Response uriParam(@PathParam("1") String one, URI endpoint, @PathParam("2") String two); + @GET @Path("/{1}/{2}") + Response uriParam(@PathParam("1") String one, URI endpoint, @PathParam("2") String two); @dagger.Module(overrides = true, library = true) static class Module { diff --git a/feign-core/src/test/java/feign/TrustingSSLSocketFactory.java b/feign-core/src/test/java/feign/TrustingSSLSocketFactory.java index 29ae665629..89e8d17cfd 100644 --- a/feign-core/src/test/java/feign/TrustingSSLSocketFactory.java +++ b/feign-core/src/test/java/feign/TrustingSSLSocketFactory.java @@ -46,7 +46,8 @@ private TrustingSSLSocketFactory() { return ENABLED_CIPHER_SUITES; } - @Override public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { + @Override + public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { return setEnabledCipherSuites(delegate.createSocket(s, host, port, autoClose)); } @@ -55,7 +56,8 @@ static Socket setEnabledCipherSuites(Socket socket) { return socket; } - @Override public Socket createSocket(String host, int port) throws IOException, UnknownHostException { + @Override + public Socket createSocket(String host, int port) throws IOException, UnknownHostException { return setEnabledCipherSuites(delegate.createSocket(host, port)); } @@ -63,12 +65,14 @@ static Socket setEnabledCipherSuites(Socket socket) { return setEnabledCipherSuites(delegate.createSocket(host, port)); } - @Override public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException { return setEnabledCipherSuites(delegate.createSocket(host, port, localHost, localPort)); } - @Override public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { return setEnabledCipherSuites(delegate.createSocket(address, port, localAddress, localPort)); } diff --git a/feign-core/src/test/java/feign/examples/GitHubExample.java b/feign-core/src/test/java/feign/examples/GitHubExample.java index 76d813e687..20af6532d1 100644 --- a/feign-core/src/test/java/feign/examples/GitHubExample.java +++ b/feign-core/src/test/java/feign/examples/GitHubExample.java @@ -32,7 +32,8 @@ public class GitHubExample { interface GitHub { - @GET @Path("/repos/{owner}/{repo}/contributors") List contributors(@PathParam("owner") String owner, @PathParam("repo") String repo); + @GET @Path("/repos/{owner}/{repo}/contributors") List contributors( + @PathParam("owner") String owner, @PathParam("repo") String repo); } static class Contributor { From 3440c6392e3756d5b3ece73c93e2351d4826ceea Mon Sep 17 00:00:00 2001 From: adriancole Date: Fri, 28 Jun 2013 08:26:22 -0700 Subject: [PATCH 047/179] pmd and findbugs sweep + license headers --- README.md | 3 +- feign-core/src/main/java/feign/Client.java | 15 +++++++ feign-core/src/main/java/feign/Contract.java | 17 +++++++- feign-core/src/main/java/feign/Feign.java | 17 +++++++- .../src/main/java/feign/FeignException.java | 20 +++++++-- .../src/main/java/feign/MethodHandler.java | 17 +++++++- .../src/main/java/feign/MethodMetadata.java | 20 +++++++-- .../src/main/java/feign/ReflectiveFeign.java | 15 +++++++ feign-core/src/main/java/feign/Request.java | 15 +++++++ .../src/main/java/feign/RequestTemplate.java | 28 ++++++++---- feign-core/src/main/java/feign/Response.java | 17 +++++++- .../main/java/feign/RetryableException.java | 31 ++++++++----- feign-core/src/main/java/feign/Retryer.java | 15 +++++++ feign-core/src/main/java/feign/Target.java | 15 +++++++ feign-core/src/main/java/feign/Wire.java | 43 +++++++++++-------- .../main/java/feign/codec/BodyEncoder.java | 19 +++++++- .../src/main/java/feign/codec/Decoder.java | 24 +++++++++-- .../src/main/java/feign/codec/Decoders.java | 17 +++++++- .../main/java/feign/codec/ErrorDecoder.java | 42 ++++++++++++------ .../main/java/feign/codec/FormEncoder.java | 21 +++++++-- .../src/main/java/feign/codec/SAXDecoder.java | 15 +++++++ .../java/feign/codec/ToStringDecoder.java | 17 +++++++- .../src/test/java/feign/ContractTest.java | 34 +++++++++++---- .../test/java/feign/DefaultRetryerTest.java | 15 +++++++ feign-core/src/test/java/feign/FeignTest.java | 20 +++++++-- .../test/java/feign/RequestTemplateTest.java | 15 +++++++ .../java/feign/TrustingSSLSocketFactory.java | 23 +++++++--- .../feign/codec/DefaultErrorDecoderTest.java | 15 +++++++ .../feign/codec/RetryAfterDecoderTest.java | 15 +++++++ .../java/feign/examples/GitHubExample.java | 20 ++++++--- .../test/java/feign/examples/IAMExample.java | 15 +++++++ 31 files changed, 518 insertions(+), 97 deletions(-) diff --git a/README.md b/README.md index 0bf18dc73a..8a6c81865f 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,8 @@ Usage typically looks like this, an adaptation of the [canonical Retrofit sample ```java interface GitHub { - @GET @Path("/repos/{owner}/{repo}/contributors") List contributors(@PathParam("owner") String owner, @PathParam("repo") String repo); + @GET @Path("/repos/{owner}/{repo}/contributors") + List contributors(@PathParam("owner") String owner, @PathParam("repo") String repo); } static class Contributor { diff --git a/feign-core/src/main/java/feign/Client.java b/feign-core/src/main/java/feign/Client.java index 5659f283c3..0fbee091ed 100644 --- a/feign-core/src/main/java/feign/Client.java +++ b/feign-core/src/main/java/feign/Client.java @@ -1,3 +1,18 @@ +/* + * 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.collect.ImmutableListMultimap; diff --git a/feign-core/src/main/java/feign/Contract.java b/feign-core/src/main/java/feign/Contract.java index ab30935dbc..f1db269c0f 100644 --- a/feign-core/src/main/java/feign/Contract.java +++ b/feign-core/src/main/java/feign/Contract.java @@ -1,3 +1,18 @@ +/* + * 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.base.Joiner; @@ -117,4 +132,4 @@ public static MethodMetadata parseAndValidatateMetadata(Method method) { } return data; } -} \ No newline at end of file +} diff --git a/feign-core/src/main/java/feign/Feign.java b/feign-core/src/main/java/feign/Feign.java index d7a13cda63..9b8bd3252c 100644 --- a/feign-core/src/main/java/feign/Feign.java +++ b/feign-core/src/main/java/feign/Feign.java @@ -1,3 +1,18 @@ +/* + * 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.base.Optional; @@ -145,4 +160,4 @@ public static String configKey(Method method) { Feign() { } -} \ No newline at end of file +} diff --git a/feign-core/src/main/java/feign/FeignException.java b/feign-core/src/main/java/feign/FeignException.java index 2375c5fa96..500c0af287 100644 --- a/feign-core/src/main/java/feign/FeignException.java +++ b/feign-core/src/main/java/feign/FeignException.java @@ -1,3 +1,18 @@ +/* + * 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.reflect.TypeToken; @@ -28,8 +43,7 @@ public static FeignException errorStatus(String methodKey, Response response) { response = Response.create(response.status(), response.reason(), response.headers(), body.toString()); message += "; content:\n" + body; } - } catch (IOException ignored) { - + } catch (IOException ignored) { // NOPMD } return new FeignException(message); } @@ -48,4 +62,4 @@ protected FeignException(String message) { } private static final long serialVersionUID = 0; -} \ No newline at end of file +} diff --git a/feign-core/src/main/java/feign/MethodHandler.java b/feign-core/src/main/java/feign/MethodHandler.java index a3a84b3d1e..e734dc11f8 100644 --- a/feign-core/src/main/java/feign/MethodHandler.java +++ b/feign-core/src/main/java/feign/MethodHandler.java @@ -1,3 +1,18 @@ +/* + * 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.base.Function; @@ -112,7 +127,7 @@ private void ensureBodyClosed(Response response) { if (response.body().isPresent()) { try { response.body().get().close(); - } catch (IOException ignored) { + } catch (IOException ignored) { // NOPMD } } } diff --git a/feign-core/src/main/java/feign/MethodMetadata.java b/feign-core/src/main/java/feign/MethodMetadata.java index 409c5e624b..e9b02700a3 100644 --- a/feign-core/src/main/java/feign/MethodMetadata.java +++ b/feign-core/src/main/java/feign/MethodMetadata.java @@ -1,3 +1,18 @@ +/* + * 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.collect.LinkedHashMultimap; @@ -6,7 +21,6 @@ import com.google.common.reflect.TypeToken; import java.io.Serializable; -import java.lang.reflect.Method; import java.util.List; public final class MethodMetadata implements Serializable { @@ -22,7 +36,7 @@ public final class MethodMetadata implements Serializable { private SetMultimap indexToName = LinkedHashMultimap.create(); /** - * @see Feign#configKey(Method) + * @see Feign#configKey(java.lang.reflect.Method) */ public String configKey() { return configKey; @@ -73,4 +87,4 @@ public SetMultimap indexToName() { } private static final long serialVersionUID = 1L; -} \ No newline at end of file +} diff --git a/feign-core/src/main/java/feign/ReflectiveFeign.java b/feign-core/src/main/java/feign/ReflectiveFeign.java index a13e0c5201..5936cd5627 100644 --- a/feign-core/src/main/java/feign/ReflectiveFeign.java +++ b/feign-core/src/main/java/feign/ReflectiveFeign.java @@ -1,3 +1,18 @@ +/* + * 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.base.Function; diff --git a/feign-core/src/main/java/feign/Request.java b/feign-core/src/main/java/feign/Request.java index 30a47d966c..eb062c8c23 100644 --- a/feign-core/src/main/java/feign/Request.java +++ b/feign-core/src/main/java/feign/Request.java @@ -1,3 +1,18 @@ +/* + * 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.base.Objects; diff --git a/feign-core/src/main/java/feign/RequestTemplate.java b/feign-core/src/main/java/feign/RequestTemplate.java index 04922447f0..b2b1d9adb5 100644 --- a/feign-core/src/main/java/feign/RequestTemplate.java +++ b/feign-core/src/main/java/feign/RequestTemplate.java @@ -1,3 +1,18 @@ +/* + * 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.base.Objects; @@ -13,7 +28,6 @@ import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; -import com.google.common.net.HttpHeaders; import java.io.Serializable; import java.io.UnsupportedEncodingException; @@ -25,9 +39,6 @@ import java.util.Map; import java.util.Map.Entry; -import feign.codec.BodyEncoder; -import feign.codec.FormEncoder; - import static com.google.common.base.Charsets.UTF_8; import static com.google.common.base.Objects.equal; import static com.google.common.base.Preconditions.checkNotNull; @@ -165,8 +176,7 @@ private static String urlEncode(Object arg) { } /** - * Expands a {@code template}, such as {@code username} - * }, using the {@code variables} supplied. Any unresolved + * Expands a {@code template}, such as {@code username}, using the {@code variables} supplied. Any unresolved * parameters will remain. *

* Note that if you'd like curly braces literally in the {@code template}, @@ -400,9 +410,9 @@ public ListMultimap headers() { } /** - * replaces the {@link HttpHeaders#CONTENT_LENGTH} header. + * replaces the {@link com.google.common.net.HttpHeaders#CONTENT_LENGTH} header. *

- * Usually populated by {@link BodyEncoder} or {@link FormEncoder} + * Usually populated by {@link feign.codec.BodyEncoder} or {@link feign.codec.FormEncoder} * * @see Request#body() */ @@ -530,4 +540,4 @@ public String queryLine() { } private static final long serialVersionUID = 1L; -} \ No newline at end of file +} diff --git a/feign-core/src/main/java/feign/Response.java b/feign-core/src/main/java/feign/Response.java index 6b68681a45..2fa628c239 100644 --- a/feign-core/src/main/java/feign/Response.java +++ b/feign-core/src/main/java/feign/Response.java @@ -1,3 +1,18 @@ +/* + * 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.base.Objects; @@ -63,7 +78,7 @@ public Optional body() { return body; } - public static interface Body extends Closeable { + public interface Body extends Closeable { /** * length in bytes, if known. diff --git a/feign-core/src/main/java/feign/RetryableException.java b/feign-core/src/main/java/feign/RetryableException.java index 8f1bcc7496..3b9d3065ea 100644 --- a/feign-core/src/main/java/feign/RetryableException.java +++ b/feign-core/src/main/java/feign/RetryableException.java @@ -1,15 +1,27 @@ +/* + * 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.base.Optional; -import com.google.common.net.HttpHeaders; import java.util.Date; -import feign.codec.ErrorDecoder; - /** * This exception is raised when the {@link Response} is deemed to be retryable, - * typically via an {@link ErrorDecoder} when the {@link Response#status() + * typically via an {@link feign.codec.ErrorDecoder} when the {@link Response#status() * status} is 503. */ public class RetryableException extends FeignException { @@ -19,8 +31,8 @@ public class RetryableException extends FeignException { private final Optional retryAfter; /** - * @param retryAfter usually corresponds to the {@link HttpHeaders#RETRY_AFTER} - * header. + * @param retryAfter usually corresponds to the {@link com.google.common.net.HttpHeaders#RETRY_AFTER} + * header. */ public RetryableException(String message, Throwable cause, Date retryAfter) { super(message, cause); @@ -28,8 +40,8 @@ public RetryableException(String message, Throwable cause, Date retryAfter) { } /** - * @param retryAfter usually corresponds to the {@link HttpHeaders#RETRY_AFTER} - * header. + * @param retryAfter usually corresponds to the {@link com.google.common.net.HttpHeaders#RETRY_AFTER} + * header. */ public RetryableException(String message, Date retryAfter) { super(message); @@ -37,12 +49,11 @@ public RetryableException(String message, Date retryAfter) { } /** - * Sometimes corresponds to the {@link HttpHeaders#RETRY_AFTER} header + * Sometimes corresponds to the {@link com.google.common.net.HttpHeaders#RETRY_AFTER} header * present in {@code 503} status. Other times parsed from an * application-specific response. */ public Optional retryAfter() { return retryAfter; } - } diff --git a/feign-core/src/main/java/feign/Retryer.java b/feign-core/src/main/java/feign/Retryer.java index 697f06c1d7..832f41377d 100644 --- a/feign-core/src/main/java/feign/Retryer.java +++ b/feign-core/src/main/java/feign/Retryer.java @@ -1,3 +1,18 @@ +/* + * 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.base.Ticker; diff --git a/feign-core/src/main/java/feign/Target.java b/feign-core/src/main/java/feign/Target.java index bd2724cb2c..16f2aba444 100644 --- a/feign-core/src/main/java/feign/Target.java +++ b/feign-core/src/main/java/feign/Target.java @@ -1,3 +1,18 @@ +/* + * 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.base.Function; diff --git a/feign-core/src/main/java/feign/Wire.java b/feign-core/src/main/java/feign/Wire.java index 76e17b929f..f63b517e15 100644 --- a/feign-core/src/main/java/feign/Wire.java +++ b/feign-core/src/main/java/feign/Wire.java @@ -1,3 +1,18 @@ +/* + * 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.Closer; @@ -12,13 +27,9 @@ import java.util.logging.Logger; import java.util.logging.SimpleFormatter; -/** - * Writes http headers and body. Plumb to your favorite log impl. - */ +/* Writes http headers and body. Plumb to your favorite log impl. */ public abstract class Wire { - /** - * logs to the category {@link Wire} at {@link Level#FINE} - */ + /* logs to the category {@link Wire} at {@link Level#FINE}. */ public static class ErrorWire extends Wire { final Logger logger = Logger.getLogger(Wire.class.getName()); @@ -27,9 +38,7 @@ public static class ErrorWire extends Wire { } } - /** - * logs to the category {@link Wire} at {@link Level#FINE}, if loggable. - */ + /* logs to the category {@link Wire} at {@link Level#FINE}, if loggable. */ public static class LoggingWire extends Wire { final Logger logger = Logger.getLogger(Wire.class.getName()); @@ -39,8 +48,7 @@ public static class LoggingWire extends Wire { } } - @Override - Response wireAndRebufferResponse(Target target, Response response) throws IOException { + @Override Response wireAndRebufferResponse(Target target, Response response) throws IOException { if (logger.isLoggable(Level.FINE)) { return super.wireAndRebufferResponse(target, response); } @@ -51,9 +59,7 @@ Response wireAndRebufferResponse(Target target, Response response) throws IOE logger.fine(String.format(format, args)); } - /** - * helper that configures jul to sanely log messages. - */ + /* helper that configures jul to sanely log messages. */ public LoggingWire appendToFile(String logfile) { final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); logger.setLevel(Level.FINE); @@ -62,8 +68,8 @@ public LoggingWire appendToFile(String logfile) { handler.setFormatter(new SimpleFormatter() { @Override public String format(LogRecord record) { - String timestamp = sdf.format(new java.util.Date(record.getMillis())); - return String.format("%s %s%n", timestamp, record.getMessage()); + String timestamp = sdf.format(new java.util.Date(record.getMillis())); // NOPMD + return String.format("%s %s%n", timestamp, record.getMessage()); // NOPMD } }); logger.addHandler(handler); @@ -78,8 +84,7 @@ public static class NoOpWire extends Wire { @Override void wireRequest(Target target, Request request) { } - @Override - Response wireAndRebufferResponse(Target target, Response response) throws IOException { + @Override Response wireAndRebufferResponse(Target target, Response response) throws IOException { return response; } @@ -138,4 +143,4 @@ Response wireAndRebufferResponse(Target target, Response response) throws IOE } return response; } -} \ No newline at end of file +} diff --git a/feign-core/src/main/java/feign/codec/BodyEncoder.java b/feign-core/src/main/java/feign/codec/BodyEncoder.java index 9f0b581de4..5ee03d5e60 100644 --- a/feign-core/src/main/java/feign/codec/BodyEncoder.java +++ b/feign-core/src/main/java/feign/codec/BodyEncoder.java @@ -1,3 +1,18 @@ +/* + * 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.codec; import feign.RequestTemplate; @@ -14,12 +29,12 @@ public interface BodyEncoder { * private final Gson gson; * * public GsonEncoder(Gson gson) { - * this.gson = gson; + * this.gson = gson; * } * * @Override * public void encodeBody(Object bodyParam, RequestTemplate base) { - * base.body(gson.toJson(bodyParam)); + * base.body(gson.toJson(bodyParam)); * } * * } diff --git a/feign-core/src/main/java/feign/codec/Decoder.java b/feign-core/src/main/java/feign/codec/Decoder.java index f35c2e4d76..3dbb910a17 100644 --- a/feign-core/src/main/java/feign/codec/Decoder.java +++ b/feign-core/src/main/java/feign/codec/Decoder.java @@ -1,3 +1,18 @@ +/* + * 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.codec; import com.google.common.io.Closer; @@ -19,12 +34,12 @@ * private final Gson gson; * * public GsonDecoder(Gson gson) { - * this.gson = gson; + * this.gson = gson; * } * * @Override * public Object decode(String methodKey, Reader reader, TypeToken<?> type) { - * return gson.fromJson(reader, type.getType()); + * return gson.fromJson(reader, type.getType()); * } * } * @@ -73,7 +88,8 @@ public Object decode(String methodKey, Response response, TypeToken type) thr * Implement this to decode a {@code Reader} to an object of the specified * type. * - * @param methodKey {@link feign.Feign#configKey} of the java method that invoked the request. ex. {@code IAM#getUser()} + * @param methodKey {@link feign.Feign#configKey} of the java method that invoked the request. + * ex. {@code IAM#getUser()} * @param reader no need to close this, as {@link #decode(String, Response, TypeToken)} * manages resources. * @param type Target object type. @@ -81,4 +97,4 @@ public Object decode(String methodKey, Response response, TypeToken type) thr * @throws Throwable will be propagated safely to the caller. */ public abstract Object decode(String methodKey, Reader reader, TypeToken type) throws Throwable; -} \ No newline at end of file +} diff --git a/feign-core/src/main/java/feign/codec/Decoders.java b/feign-core/src/main/java/feign/codec/Decoders.java index 54a7973755..e8140b2d6e 100644 --- a/feign-core/src/main/java/feign/codec/Decoders.java +++ b/feign-core/src/main/java/feign/codec/Decoders.java @@ -1,3 +1,18 @@ +/* + * 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.codec; import com.google.common.base.Function; @@ -120,4 +135,4 @@ public List decode(String methodKey, Reader reader, TypeToken type) throws public static Decoder eachFirstGroup(String pattern) { return transformEachFirstGroup(pattern, Functions.identity()); } -} \ No newline at end of file +} diff --git a/feign-core/src/main/java/feign/codec/ErrorDecoder.java b/feign-core/src/main/java/feign/codec/ErrorDecoder.java index 7651d3ec50..0ffd9cfbb9 100644 --- a/feign-core/src/main/java/feign/codec/ErrorDecoder.java +++ b/feign-core/src/main/java/feign/codec/ErrorDecoder.java @@ -1,9 +1,23 @@ +/* + * 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.codec; import com.google.common.base.Function; import com.google.common.base.Optional; import com.google.common.base.Ticker; -import com.google.common.net.HttpHeaders; import com.google.common.reflect.TypeToken; import java.text.DateFormat; @@ -36,9 +50,9 @@ * * @Override * public Object decode(String methodKey, Response response, TypeToken<?> type) throws Throwable { - * if (response.status() == 404) - * throw new IllegalArgumentException("zone not found"); - * return ErrorDecoder.DEFAULT.decode(request, response, type); + * if (response.status() == 404) + * throw new IllegalArgumentException("zone not found"); + * return ErrorDecoder.DEFAULT.decode(request, response, type); * } * * } @@ -55,13 +69,13 @@ public interface ErrorDecoder { * * @param methodKey {@link feign.Feign#configKey} of the java method that invoked the request. ex. {@code IAM#getUser()} * @param response HTTP response where {@link Response#status() status} >= - * {@code 300}. + * {@code 300}. * @param type Target object type. * @return instance of {@code type} * @throws Throwable IOException, if there was a network error reading the - * response or an application-specific exception decoded by the - * implementation. If the throwable is retryable, it should be - * wrapped, or a subtype of {@link RetryableException} + * response or an application-specific exception decoded by the + * implementation. If the throwable is retryable, it should be + * wrapped, or a subtype of {@link RetryableException} */ public Object decode(String methodKey, Response response, TypeToken type) throws Throwable; @@ -80,12 +94,12 @@ public Object decode(String methodKey, Response response, TypeToken type) thr }; /** - * Decodes a {@link HttpHeaders#RETRY_AFTER} header into an absolute date, + * Decodes a {@link com.google.common.net.HttpHeaders#RETRY_AFTER} header into an absolute date, * if possible. * * @see Retry-After - * format + * href="https://tools.ietf.org/html/rfc2616#section-14.37">Retry-After + * format */ static class RetryAfterDecoder implements Function> { static final DateFormat RFC822_FORMAT = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", US); @@ -106,8 +120,8 @@ static class RetryAfterDecoder implements Function> { * retried. * * @param retryAfter String in Retry-After format + * href="https://tools.ietf.org/html/rfc2616#section-14.37" + * >Retry-After format */ @Override public Optional apply(String retryAfter) { @@ -127,4 +141,4 @@ public Optional apply(String retryAfter) { } } } -} \ No newline at end of file +} diff --git a/feign-core/src/main/java/feign/codec/FormEncoder.java b/feign-core/src/main/java/feign/codec/FormEncoder.java index a03d68e78d..08e77a43bb 100644 --- a/feign-core/src/main/java/feign/codec/FormEncoder.java +++ b/feign-core/src/main/java/feign/codec/FormEncoder.java @@ -1,9 +1,22 @@ +/* + * 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.codec; import java.util.Map; -import javax.ws.rs.FormParam; - import feign.RequestTemplate; public interface FormEncoder { @@ -11,7 +24,7 @@ public interface FormEncoder { /** * FormParam encoding *

- * If any parameters are annotated with {@link FormParam}, they will be + * If any parameters are annotated with {@link javax.ws.rs.FormParam}, they will be * collected and passed as {code formParams} *

*

@@ -24,4 +37,4 @@ public interface FormEncoder {
    * @param base       template to encode the {@code object} into.
    */
   void encodeForm(Map formParams, RequestTemplate base);
-}
\ No newline at end of file
+}
diff --git a/feign-core/src/main/java/feign/codec/SAXDecoder.java b/feign-core/src/main/java/feign/codec/SAXDecoder.java
index 1a0184c76d..5a36b2a13e 100644
--- a/feign-core/src/main/java/feign/codec/SAXDecoder.java
+++ b/feign-core/src/main/java/feign/codec/SAXDecoder.java
@@ -1,3 +1,18 @@
+/*
+ * 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.codec;
 
 import com.google.common.reflect.TypeToken;
diff --git a/feign-core/src/main/java/feign/codec/ToStringDecoder.java b/feign-core/src/main/java/feign/codec/ToStringDecoder.java
index 1c4a7948cb..72413d66e5 100644
--- a/feign-core/src/main/java/feign/codec/ToStringDecoder.java
+++ b/feign-core/src/main/java/feign/codec/ToStringDecoder.java
@@ -1,3 +1,18 @@
+/*
+ * 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.codec;
 
 import com.google.common.io.CharStreams;
@@ -10,4 +25,4 @@ public class ToStringDecoder extends Decoder {
   public Object decode(String methodKey, Reader reader, TypeToken type) throws Throwable {
     return CharStreams.toString(reader);
   }
-}
\ No newline at end of file
+}
diff --git a/feign-core/src/test/java/feign/ContractTest.java b/feign-core/src/test/java/feign/ContractTest.java
index 9e2b1ed0eb..faec7ff527 100644
--- a/feign-core/src/test/java/feign/ContractTest.java
+++ b/feign-core/src/test/java/feign/ContractTest.java
@@ -1,3 +1,18 @@
+/*
+ * 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.collect.ImmutableList;
@@ -30,10 +45,14 @@
 import static org.testng.Assert.assertFalse;
 import static org.testng.Assert.assertTrue;
 
+/**
+ * Tests interfaces defined per {@link Contract} are interpreted into expected {@link RequestTemplate template}
+ * instances.
+ */
 @Test
 public class ContractTest {
 
-  static interface Methods {
+  interface Methods {
     @POST void post();
 
     @PUT void put();
@@ -50,7 +69,7 @@ static interface Methods {
     assertEquals(parseAndValidatateMetadata(Methods.class.getDeclaredMethod("delete")).template().method(), DELETE);
   }
 
-  static interface WithQueryParamsInPath {
+  interface WithQueryParamsInPath {
     @GET @Path("/?Action=GetUser&Version=2010-05-08") Response get();
   }
 
@@ -61,7 +80,7 @@ static interface WithQueryParamsInPath {
     assertEquals(md.template().queries().get("Version"), ImmutableSet.of("2010-05-08"));
   }
 
-  static interface BodyWithoutParameters {
+  interface BodyWithoutParameters {
     @POST @Produces(APPLICATION_XML) @Body("") Response post();
   }
 
@@ -78,9 +97,8 @@ static interface BodyWithoutParameters {
     assertEquals(md.template().headers().get(CONTENT_TYPE), ImmutableSet.of(APPLICATION_XML));
   }
 
-  static interface WithURIParam {
-    @GET @Path("/{1}/{2}")
-    Response uriParam(@PathParam("1") String one, URI endpoint, @PathParam("2") String two);
+  interface WithURIParam {
+    @GET @Path("/{1}/{2}") Response uriParam(@PathParam("1") String one, URI endpoint, @PathParam("2") String two);
   }
 
   @Test public void methodCanHaveUriParam() throws Exception {
@@ -97,7 +115,7 @@ static interface WithURIParam {
     assertEquals(md.indexToName().get(2), ImmutableSet.of("2"));
   }
 
-  static interface FormParams {
+  interface FormParams {
     @POST
     @Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D")
     void login(
@@ -118,7 +136,7 @@ void login(
     assertEquals(md.indexToName().get(2), ImmutableSet.of("password"));
   }
 
-  static interface HeaderParams {
+  interface HeaderParams {
     @POST void logout(@HeaderParam("Auth-Token") String token);
   }
 
diff --git a/feign-core/src/test/java/feign/DefaultRetryerTest.java b/feign-core/src/test/java/feign/DefaultRetryerTest.java
index 337125d88b..57cd1b7346 100644
--- a/feign-core/src/test/java/feign/DefaultRetryerTest.java
+++ b/feign-core/src/test/java/feign/DefaultRetryerTest.java
@@ -1,3 +1,18 @@
+/*
+ * 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.base.Ticker;
diff --git a/feign-core/src/test/java/feign/FeignTest.java b/feign-core/src/test/java/feign/FeignTest.java
index 5db23bfe06..7c8daaf437 100644
--- a/feign-core/src/test/java/feign/FeignTest.java
+++ b/feign-core/src/test/java/feign/FeignTest.java
@@ -1,3 +1,18 @@
+/*
+ * 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.collect.ImmutableMap;
@@ -30,11 +45,10 @@
 
 @Test
 public class FeignTest {
-  static interface TestInterface {
+  interface TestInterface {
     @POST String post();
 
-    @GET @Path("/{1}/{2}")
-    Response uriParam(@PathParam("1") String one, URI endpoint, @PathParam("2") String two);
+    @GET @Path("/{1}/{2}") Response uriParam(@PathParam("1") String one, URI endpoint, @PathParam("2") String two);
 
     @dagger.Module(overrides = true, library = true)
     static class Module {
diff --git a/feign-core/src/test/java/feign/RequestTemplateTest.java b/feign-core/src/test/java/feign/RequestTemplateTest.java
index 881fb08854..c28e35d45e 100644
--- a/feign-core/src/test/java/feign/RequestTemplateTest.java
+++ b/feign-core/src/test/java/feign/RequestTemplateTest.java
@@ -1,3 +1,18 @@
+/*
+ * 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.collect.ImmutableList;
diff --git a/feign-core/src/test/java/feign/TrustingSSLSocketFactory.java b/feign-core/src/test/java/feign/TrustingSSLSocketFactory.java
index 89e8d17cfd..fc08cc13bc 100644
--- a/feign-core/src/test/java/feign/TrustingSSLSocketFactory.java
+++ b/feign-core/src/test/java/feign/TrustingSSLSocketFactory.java
@@ -1,9 +1,23 @@
+/*
+ * 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.net.UnknownHostException;
 import java.security.SecureRandom;
 import java.security.cert.X509Certificate;
 
@@ -57,7 +71,7 @@ static Socket setEnabledCipherSuites(Socket socket) {
   }
 
   @Override
-  public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
+  public Socket createSocket(String host, int port) throws IOException {
     return setEnabledCipherSuites(delegate.createSocket(host, port));
   }
 
@@ -66,8 +80,7 @@ public Socket createSocket(String host, int port) throws IOException, UnknownHos
   }
 
   @Override
-  public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException,
-      UnknownHostException {
+  public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
     return setEnabledCipherSuites(delegate.createSocket(host, port, localHost, localPort));
   }
 
@@ -82,11 +95,9 @@ public X509Certificate[] getAcceptedIssuers() {
   }
 
   public void checkClientTrusted(X509Certificate[] certs, String authType) {
-    return;
   }
 
   public void checkServerTrusted(X509Certificate[] certs, String authType) {
-    return;
   }
 
   private final static String[] ENABLED_CIPHER_SUITES = {"SSL_DH_anon_WITH_RC4_128_MD5"};
diff --git a/feign-core/src/test/java/feign/codec/DefaultErrorDecoderTest.java b/feign-core/src/test/java/feign/codec/DefaultErrorDecoderTest.java
index 502764560f..90734c52cf 100644
--- a/feign-core/src/test/java/feign/codec/DefaultErrorDecoderTest.java
+++ b/feign-core/src/test/java/feign/codec/DefaultErrorDecoderTest.java
@@ -1,3 +1,18 @@
+/*
+ * 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.codec;
 
 import com.google.common.collect.ImmutableListMultimap;
diff --git a/feign-core/src/test/java/feign/codec/RetryAfterDecoderTest.java b/feign-core/src/test/java/feign/codec/RetryAfterDecoderTest.java
index a9be5b2df3..1f4e473df5 100644
--- a/feign-core/src/test/java/feign/codec/RetryAfterDecoderTest.java
+++ b/feign-core/src/test/java/feign/codec/RetryAfterDecoderTest.java
@@ -1,3 +1,18 @@
+/*
+ * 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.codec;
 
 import com.google.common.base.Ticker;
diff --git a/feign-core/src/test/java/feign/examples/GitHubExample.java b/feign-core/src/test/java/feign/examples/GitHubExample.java
index 20af6532d1..26cfc74253 100644
--- a/feign-core/src/test/java/feign/examples/GitHubExample.java
+++ b/feign-core/src/test/java/feign/examples/GitHubExample.java
@@ -1,3 +1,18 @@
+/*
+ * 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.examples;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
@@ -20,7 +35,6 @@
 import dagger.Provides;
 import feign.Feign;
 import feign.codec.Decoder;
-import feign.codec.Decoders;
 
 import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY;
 import static com.fasterxml.jackson.annotation.PropertyAccessor.FIELD;
@@ -53,8 +67,6 @@ public static void main(String... args) {
 
   /**
    * Here's how to wire gson deserialization.
-   *
-   * @see Decoders
    */
   @Module(overrides = true, library = true)
   static class GsonModule {
@@ -73,8 +85,6 @@ static class GsonModule {
 
   /**
    * Here's how to wire jackson deserialization.
-   *
-   * @see Decoders
    */
   @Module(overrides = true, library = true)
   static class JacksonModule {
diff --git a/feign-core/src/test/java/feign/examples/IAMExample.java b/feign-core/src/test/java/feign/examples/IAMExample.java
index c63faf2caa..0d3b6eeb38 100644
--- a/feign-core/src/test/java/feign/examples/IAMExample.java
+++ b/feign-core/src/test/java/feign/examples/IAMExample.java
@@ -1,3 +1,18 @@
+/*
+ * 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.examples;
 
 import com.google.common.base.Function;

From f5fe7104f2de5286184c358fccb20207c43df9c7 Mon Sep 17 00:00:00 2001
From: adriancole 
Date: Thu, 27 Jun 2013 11:36:29 -0700
Subject: [PATCH 048/179] Added Ribbon integration

---
 CHANGES.md                                    |   6 +
 README.md                                     |  10 ++
 build.gradle                                  |  24 ++-
 feign-ribbon/README.md                        |  30 ++++
 .../src/main/java/feign/ribbon/LBClient.java  | 153 ++++++++++++++++++
 .../feign/ribbon/LoadBalancingTarget.java     | 114 +++++++++++++
 .../main/java/feign/ribbon/RibbonModule.java  |  89 ++++++++++
 .../feign/ribbon/LoadBalancingTargetTest.java |  74 +++++++++
 .../java/feign/ribbon/RibbonClientTest.java   |  74 +++++++++
 settings.gradle                               |   2 +-
 10 files changed, 572 insertions(+), 4 deletions(-)
 create mode 100644 CHANGES.md
 create mode 100644 feign-ribbon/README.md
 create mode 100644 feign-ribbon/src/main/java/feign/ribbon/LBClient.java
 create mode 100644 feign-ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java
 create mode 100644 feign-ribbon/src/main/java/feign/ribbon/RibbonModule.java
 create mode 100644 feign-ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java
 create mode 100644 feign-ribbon/src/test/java/feign/ribbon/RibbonClientTest.java

diff --git a/CHANGES.md b/CHANGES.md
new file mode 100644
index 0000000000..0541be1724
--- /dev/null
+++ b/CHANGES.md
@@ -0,0 +1,6 @@
+### Version 1.1.0
+* adds Ribbon integration
+
+### Version 1.0.0
+
+* Initial open source release
diff --git a/README.md b/README.md
index 8a6c81865f..a370808e7e 100644
--- a/README.md
+++ b/README.md
@@ -65,6 +65,16 @@ CloudDNS cloudDNS =  Feign.create().newInstance(new CloudIdentityTarget {
+
+  private final Client delegate;
+  private final int connectTimeout;
+  private final int readTimeout;
+
+  LBClient(Client delegate, ILoadBalancer lb, IClientConfig clientConfig) {
+    this.delegate = delegate;
+    this.connectTimeout = Integer.valueOf(clientConfig.getProperty(ConnectTimeout).toString());
+    this.readTimeout = Integer.valueOf(clientConfig.getProperty(ReadTimeout).toString());
+    setLoadBalancer(lb);
+    initWithNiwsConfig(clientConfig);
+  }
+
+  @Override
+  public RibbonResponse execute(RibbonRequest request) throws IOException {
+    int connectTimeout = config(request, ConnectTimeout, this.connectTimeout);
+    int readTimeout = config(request, ReadTimeout, this.readTimeout);
+
+    Request.Options options = new Request.Options(connectTimeout, readTimeout);
+    Response response = delegate.execute(request.toRequest(), options);
+    return new RibbonResponse(request.getUri(), response);
+  }
+
+  @Override protected boolean isCircuitBreakerException(Exception e) {
+    return e instanceof IOException;
+  }
+
+  @Override protected boolean isRetriableException(Exception e) {
+    return e instanceof RetryableException;
+  }
+
+  @Override
+  protected Pair deriveSchemeAndPortFromPartialUri(RibbonRequest task) {
+    return new Pair(URI.create(task.request.url()).getScheme(), task.getUri().getPort());
+  }
+
+  @Override protected int getDefaultPort() {
+    return 443;
+  }
+
+  static class RibbonRequest extends ClientRequest implements Cloneable {
+
+    private final Request request;
+
+    RibbonRequest(Request request, URI uri) {
+      this.request = request;
+      setUri(uri);
+    }
+
+    Request toRequest() {
+      return new RequestTemplate()
+          .method(request.method())
+          .append(getUri().toASCIIString())
+          .headers(request.headers())
+          .body(request.body().orNull()).request();
+    }
+
+    public Object clone() {
+      return new RibbonRequest(request, getUri());
+    }
+  }
+
+  static class RibbonResponse implements IResponse {
+
+    private final URI uri;
+    private final Response response;
+
+    RibbonResponse(URI uri, Response response) {
+      this.uri = uri;
+      this.response = response;
+    }
+
+    @Override public Object getPayload() throws ClientException {
+      return response.body().orNull();
+    }
+
+    @Override public boolean hasPayload() {
+      return response.body().isPresent();
+    }
+
+    @Override public boolean isSuccess() {
+      return response.status() == 200;
+    }
+
+    @Override public URI getRequestedURI() {
+      return uri;
+    }
+
+    @Override public Map> getHeaders() {
+      return response.headers().asMap();
+    }
+
+    Response toResponse() {
+      return response;
+    }
+  }
+
+  static int config(RibbonRequest request, CommonClientConfigKey key, int defaultValue) {
+    if (request.getOverrideConfig() != null && request.getOverrideConfig().containsProperty(key))
+      return Integer.valueOf(request.getOverrideConfig().getProperty(key).toString());
+    return defaultValue;
+  }
+}
diff --git a/feign-ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java b/feign-ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java
new file mode 100644
index 0000000000..337793ff2c
--- /dev/null
+++ b/feign-ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java
@@ -0,0 +1,114 @@
+/*
+ * 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.ribbon;
+
+import com.google.common.base.Objects;
+import com.netflix.loadbalancer.AbstractLoadBalancer;
+import com.netflix.loadbalancer.Server;
+
+import java.net.URI;
+
+import feign.Request;
+import feign.RequestTemplate;
+import feign.Target;
+
+import static com.google.common.base.Objects.equal;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.netflix.client.ClientFactory.getNamedLoadBalancer;
+import static java.lang.String.format;
+
+/**
+ * Basic integration for {@link com.netflix.loadbalancer.ILoadBalancer loadbalancer-aware} targets.
+ * Using this will enable dynamic url discovery via ribbon including incrementing server request counts.
+ * 

+ * Ex. + *

+ * MyService api = Feign.create(LoadBalancingTarget.create(MyService.class, "http://myAppProd"))
+ * 
+ * Where {@code myAppProd} is the ribbon loadbalancer name and {@code myAppProd.ribbon.listOfServers} configuration + * is set. + * + * @param corresponds to {@link feign.Target#type()} + */ +public class LoadBalancingTarget implements Target { + + /** + * creates a target which dynamically derives urls from a {@link com.netflix.loadbalancer.ILoadBalancer loadbalancer}. + * + * @param type corresponds to {@link feign.Target#type()} + * @param schemeName naming convention is {@code https://name} or {@code http://name} where + * name corresponds to {@link com.netflix.client.ClientFactory#getNamedLoadBalancer(String)} + */ + public static LoadBalancingTarget create(Class type, String schemeName) { + URI asUri = URI.create(schemeName); + return new LoadBalancingTarget(type, asUri.getScheme(), asUri.getHost()); + } + + private final String name; + private final String scheme; + private final Class type; + private final AbstractLoadBalancer lb; + + protected LoadBalancingTarget(Class type, String scheme, String name) { + this.type = checkNotNull(type, "type"); + this.scheme = checkNotNull(scheme, "scheme"); + this.name = checkNotNull(name, "name"); + this.lb = AbstractLoadBalancer.class.cast(getNamedLoadBalancer(name())); + } + + @Override public Class type() { + return type; + } + + @Override public String name() { + return name; + } + + @Override public String url() { + return name; + } + + /** + * current load balancer for the target. + */ + public AbstractLoadBalancer lb() { + return lb; + } + + @Override public Request apply(RequestTemplate input) { + Server currentServer = lb.chooseServer(null); + String url = format("%s://%s", scheme, currentServer.getHostPort()); + input.insert(0, url); + try { + return input.request(); + } finally { + lb.getLoadBalancerStats().incrementNumRequests(currentServer); + } + } + + @Override public int hashCode() { + return Objects.hashCode(type, name); + } + + @Override public boolean equals(Object obj) { + if (this == obj) + return true; + if (LoadBalancingTarget.class != obj.getClass()) + return false; + LoadBalancingTarget that = LoadBalancingTarget.class.cast(obj); + return equal(this.type, that.type) && equal(this.name, that.name); + } +} diff --git a/feign-ribbon/src/main/java/feign/ribbon/RibbonModule.java b/feign-ribbon/src/main/java/feign/ribbon/RibbonModule.java new file mode 100644 index 0000000000..aadc0d68e6 --- /dev/null +++ b/feign-ribbon/src/main/java/feign/ribbon/RibbonModule.java @@ -0,0 +1,89 @@ +/* + * 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.ribbon; + +import com.google.common.base.Throwables; +import com.netflix.client.ClientException; +import com.netflix.client.ClientFactory; +import com.netflix.client.config.IClientConfig; +import com.netflix.loadbalancer.ILoadBalancer; + +import java.io.IOException; +import java.net.URI; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import dagger.Provides; +import feign.Client; +import feign.Request; +import feign.Response; + +/** + * Adding this module will override URL resolution of {@link feign.Client Feign's client}, + * adding smart routing and resiliency capabilities provided by Ribbon. + *

+ * When using this, ensure the {@link feign.Target#url()} is set to as {@code http://clientName} + * or {@code https://clientName}. {@link com.netflix.client.config.IClientConfig#getClientName() clientName} + * will lookup the real url and port of your service dynamically. + *

+ * Ex. + *

+ * MyService api = Feign.create(MyService.class, "http://myAppProd", new RibbonModule());
+ * 
+ * Where {@code myAppProd} is the ribbon client name and {@code myAppProd.ribbon.listOfServers} configuration + * is set. + */ +@dagger.Module(overrides = true, library = true, complete = false) +public class RibbonModule { + + @Provides @Named("delegate") Client delegate(Client.Default delegate) { + return delegate; + } + + @Provides @Singleton Client httpClient(RibbonClient ribbon) { + return ribbon; + } + + @Singleton + static class RibbonClient implements Client { + private final Client delegate; + + @Inject + public RibbonClient(@Named("delegate") Client delegate) { + this.delegate = delegate; + } + + @Override public Response execute(Request request, Request.Options options) throws IOException { + try { + URI asUri = URI.create(request.url()); + String clientName = asUri.getHost(); + URI uriWithoutSchemeAndPort = URI.create(request.url().replace(asUri.getScheme() + "://" + asUri.getHost(), "")); + LBClient.RibbonRequest ribbonRequest = new LBClient.RibbonRequest(request, uriWithoutSchemeAndPort); + return lbClient(clientName).executeWithLoadBalancer(ribbonRequest).toResponse(); + } catch (ClientException e) { + throw Throwables.propagate(e); + } + } + + private LBClient lbClient(String clientName) { + IClientConfig config = ClientFactory.getNamedConfig(clientName); + ILoadBalancer lb = ClientFactory.getNamedLoadBalancer(clientName); + return new LBClient(delegate, lb, config); + } + } +} diff --git a/feign-ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java b/feign-ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java new file mode 100644 index 0000000000..9bf32a97ea --- /dev/null +++ b/feign-ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java @@ -0,0 +1,74 @@ +/* + * 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.ribbon; + +import com.google.mockwebserver.MockResponse; +import com.google.mockwebserver.MockWebServer; + +import org.testng.annotations.Test; + +import java.io.IOException; +import java.net.URL; + +import javax.ws.rs.POST; + +import feign.Feign; + +import static com.netflix.config.ConfigurationManager.getConfigInstance; +import static org.testng.Assert.assertEquals; + +@Test +public class LoadBalancingTargetTest { + static interface TestInterface { + @POST void post(); + } + + @Test + public void loadBalancingDefaultPolicyRoundRobin() throws IOException, InterruptedException { + String name = "LoadBalancingTargetTest-loadBalancingDefaultPolicyRoundRobin"; + String serverListKey = name + ".ribbon.listOfServers"; + + MockWebServer server1 = new MockWebServer(); + server1.enqueue(new MockResponse().setResponseCode(200).setBody("success!".getBytes())); + server1.play(); + MockWebServer server2 = new MockWebServer(); + server2.enqueue(new MockResponse().setResponseCode(200).setBody("success!".getBytes())); + server2.play(); + + getConfigInstance().setProperty(serverListKey, hostAndPort(server1.getUrl("")) + "," + hostAndPort(server2.getUrl(""))); + + try { + LoadBalancingTarget target = LoadBalancingTarget.create(TestInterface.class, "http://" + name); + TestInterface api = Feign.create(target); + + api.post(); + api.post(); + + assertEquals(server1.getRequestCount(), 1); + assertEquals(server2.getRequestCount(), 1); + // TODO: verify ribbon stats match + // assertEquals(target.lb().getLoadBalancerStats().getSingleServerStat()) + } finally { + server1.shutdown(); + server2.shutdown(); + getConfigInstance().clearProperty(serverListKey); + } + } + + static String hostAndPort(URL url) { + return url.getHost() + ":" + url.getPort(); + } +} diff --git a/feign-ribbon/src/test/java/feign/ribbon/RibbonClientTest.java b/feign-ribbon/src/test/java/feign/ribbon/RibbonClientTest.java new file mode 100644 index 0000000000..4e073b0ee1 --- /dev/null +++ b/feign-ribbon/src/test/java/feign/ribbon/RibbonClientTest.java @@ -0,0 +1,74 @@ +/* + * 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.ribbon; + +import com.google.mockwebserver.MockResponse; +import com.google.mockwebserver.MockWebServer; + +import org.testng.annotations.Test; + +import java.io.IOException; +import java.net.URL; + +import javax.ws.rs.POST; + +import feign.Feign; + +import static com.netflix.config.ConfigurationManager.getConfigInstance; +import static org.testng.Assert.assertEquals; + +@Test +public class RibbonClientTest { + static interface TestInterface { + @POST void post(); + } + + @Test + public void loadBalancingDefaultPolicyRoundRobin() throws IOException, InterruptedException { + String client = "RibbonClientTest-loadBalancingDefaultPolicyRoundRobin"; + String serverListKey = client + ".ribbon.listOfServers"; + + MockWebServer server1 = new MockWebServer(); + server1.enqueue(new MockResponse().setResponseCode(200).setBody("success!".getBytes())); + server1.play(); + MockWebServer server2 = new MockWebServer(); + server2.enqueue(new MockResponse().setResponseCode(200).setBody("success!".getBytes())); + server2.play(); + + getConfigInstance().setProperty(serverListKey, hostAndPort(server1.getUrl("")) + "," + hostAndPort(server2.getUrl(""))); + + try { + + TestInterface api = Feign.create(TestInterface.class, "http://" + client, new RibbonModule()); + + api.post(); + api.post(); + + assertEquals(server1.getRequestCount(), 1); + assertEquals(server2.getRequestCount(), 1); + // TODO: verify ribbon stats match + // assertEquals(target.lb().getLoadBalancerStats().getSingleServerStat()) + } finally { + server1.shutdown(); + server2.shutdown(); + getConfigInstance().clearProperty(serverListKey); + } + } + + static String hostAndPort(URL url) { + return url.getHost() + ":" + url.getPort(); + } +} diff --git a/settings.gradle b/settings.gradle index a98b5acfa4..df86e405ec 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ rootProject.name='feign' -include 'feign-core' +include 'feign-core', 'feign-ribbon' From ef9700b7c3f2e10ea7dc56577245d3101f1f3ed2 Mon Sep 17 00:00:00 2001 From: adriancole Date: Fri, 28 Jun 2013 15:13:04 -0700 Subject: [PATCH 049/179] workaround test failures due to invalid hostnames on jenkins slaves --- .../src/test/java/feign/ribbon/LoadBalancingTargetTest.java | 3 ++- feign-ribbon/src/test/java/feign/ribbon/RibbonClientTest.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/feign-ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java b/feign-ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java index 9bf32a97ea..aab95c05c2 100644 --- a/feign-ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java +++ b/feign-ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java @@ -69,6 +69,7 @@ public void loadBalancingDefaultPolicyRoundRobin() throws IOException, Interrupt } static String hostAndPort(URL url) { - return url.getHost() + ":" + url.getPort(); + // our build slaves have underscores in their hostnames which aren't permitted by ribbon + return "localhost:" + url.getPort(); } } diff --git a/feign-ribbon/src/test/java/feign/ribbon/RibbonClientTest.java b/feign-ribbon/src/test/java/feign/ribbon/RibbonClientTest.java index 4e073b0ee1..4fdd6ff7c4 100644 --- a/feign-ribbon/src/test/java/feign/ribbon/RibbonClientTest.java +++ b/feign-ribbon/src/test/java/feign/ribbon/RibbonClientTest.java @@ -69,6 +69,7 @@ public void loadBalancingDefaultPolicyRoundRobin() throws IOException, Interrupt } static String hostAndPort(URL url) { - return url.getHost() + ":" + url.getPort(); + // our build slaves have underscores in their hostnames which aren't permitted by ribbon + return "localhost:" + url.getPort(); } } From cc555a0fc212b945732a1f9f24731a9bfaed6a68 Mon Sep 17 00:00:00 2001 From: adriancole Date: Sun, 30 Jun 2013 12:48:26 -0700 Subject: [PATCH 050/179] bumped exponential backoff in Retryer.Default and made it possible to adjust. --- CHANGES.md | 1 + feign-core/src/main/java/feign/Retryer.java | 22 +++++++++++++------ .../test/java/feign/DefaultRetryerTest.java | 8 +++---- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 0541be1724..8456fa5db4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ ### Version 1.1.0 * adds Ribbon integration +* exponential backoff customizable via Retryer.Default ctor ### Version 1.0.0 diff --git a/feign-core/src/main/java/feign/Retryer.java b/feign-core/src/main/java/feign/Retryer.java index 832f41377d..a18cd421e4 100644 --- a/feign-core/src/main/java/feign/Retryer.java +++ b/feign-core/src/main/java/feign/Retryer.java @@ -37,19 +37,27 @@ public interface Retryer { void continueOrPropagate(RetryableException e); public static class Default implements Retryer { - private final int maxAttempts = 5; - private final long period = MILLISECONDS.toNanos(50); - private final long maxPeriod = SECONDS.toNanos(1); - // visible for testing; - Ticker ticker = Ticker.systemTicker(); - int attempt; - long sleptForNanos; + private final int maxAttempts; + private final long period; + private final long maxPeriod; public Default() { + this(MILLISECONDS.toNanos(100), SECONDS.toNanos(1), 5); + } + + public Default(long period, long maxPeriod, int maxAttempts) { + this.period = period; + this.maxPeriod = maxPeriod; + this.maxAttempts = maxAttempts; this.attempt = 1; } + // visible for testing; + Ticker ticker = Ticker.systemTicker(); + int attempt; + long sleptForNanos; + public void continueOrPropagate(RetryableException e) { if (attempt++ >= maxAttempts) throw e; diff --git a/feign-core/src/test/java/feign/DefaultRetryerTest.java b/feign-core/src/test/java/feign/DefaultRetryerTest.java index 57cd1b7346..c36cca6dc7 100644 --- a/feign-core/src/test/java/feign/DefaultRetryerTest.java +++ b/feign-core/src/test/java/feign/DefaultRetryerTest.java @@ -37,19 +37,19 @@ public void only5TriesAllowedAndExponentialBackoff() throws Exception { retryer.continueOrPropagate(e); assertEquals(retryer.attempt, 2); - assertEquals(retryer.sleptForNanos, 75000000); + assertEquals(retryer.sleptForNanos, 150000000); retryer.continueOrPropagate(e); assertEquals(retryer.attempt, 3); - assertEquals(retryer.sleptForNanos, 187500000); + assertEquals(retryer.sleptForNanos, 375000000); retryer.continueOrPropagate(e); assertEquals(retryer.attempt, 4); - assertEquals(retryer.sleptForNanos, 356250000); + assertEquals(retryer.sleptForNanos, 712500000); retryer.continueOrPropagate(e); assertEquals(retryer.attempt, 5); - assertEquals(retryer.sleptForNanos, 609375000); + assertEquals(retryer.sleptForNanos, 1218750000); retryer.continueOrPropagate(e); // fail From f82aa78d5c8b3612f84f15e03e68b222e073273a Mon Sep 17 00:00:00 2001 From: adriancole Date: Sun, 30 Jun 2013 13:40:24 -0700 Subject: [PATCH 051/179] added cli example --- CHANGES.md | 1 + examples/feign-example-cli/build.gradle | 49 ++++++++++++ .../java/feign/example/cli/GitHubExample.java | 78 +++++++++++++++++++ settings.gradle | 2 +- 4 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 examples/feign-example-cli/build.gradle create mode 100644 examples/feign-example-cli/src/main/java/feign/example/cli/GitHubExample.java diff --git a/CHANGES.md b/CHANGES.md index 8456fa5db4..396970cd87 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ ### Version 1.1.0 * adds Ribbon integration +* adds cli example * exponential backoff customizable via Retryer.Default ctor ### Version 1.0.0 diff --git a/examples/feign-example-cli/build.gradle b/examples/feign-example-cli/build.gradle new file mode 100644 index 0000000000..893ddf9e04 --- /dev/null +++ b/examples/feign-example-cli/build.gradle @@ -0,0 +1,49 @@ +apply plugin: 'java' + +dependencies { + compile 'com.netflix.feign:feign-core:1.0.0' + compile 'com.google.code.gson:gson:2.2.4' + 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.cli.GitHubExample' + } + + // 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}/github") + 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 +} diff --git a/examples/feign-example-cli/src/main/java/feign/example/cli/GitHubExample.java b/examples/feign-example-cli/src/main/java/feign/example/cli/GitHubExample.java new file mode 100644 index 0000000000..3b7657c4fd --- /dev/null +++ b/examples/feign-example-cli/src/main/java/feign/example/cli/GitHubExample.java @@ -0,0 +1,78 @@ +/* + * 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.common.collect.ImmutableMap; +import com.google.common.reflect.TypeToken; +import com.google.gson.Gson; + +import java.io.Reader; +import java.util.List; +import java.util.Map; + +import javax.inject.Singleton; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; + +import dagger.Module; +import dagger.Provides; +import feign.Feign; +import feign.codec.Decoder; + +/** + * adapted from {@code com.example.retrofit.GitHubClient} + */ +public class GitHubExample { + + interface GitHub { + @GET @Path("/repos/{owner}/{repo}/contributors") + List contributors(@PathParam("owner") String owner, @PathParam("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 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 decoders() { + return ImmutableMap.of("GitHub", jsonDecoder); + } + + final Decoder jsonDecoder = new Decoder() { + Gson gson = new Gson(); + + @Override public Object decode(String methodKey, Reader reader, TypeToken type) { + return gson.fromJson(reader, type.getType()); + } + }; + } +} diff --git a/settings.gradle b/settings.gradle index df86e405ec..d2dc7844e3 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ rootProject.name='feign' -include 'feign-core', 'feign-ribbon' +include 'feign-core', 'feign-ribbon', 'examples:feign-example-cli' From 316f4b9a2aa1333f4e995bcae529038d1cc98c45 Mon Sep 17 00:00:00 2001 From: adriancole Date: Mon, 1 Jul 2013 08:29:49 -0700 Subject: [PATCH 052/179] bumped example to 1.1.1 --- examples/feign-example-cli/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/feign-example-cli/build.gradle b/examples/feign-example-cli/build.gradle index 893ddf9e04..a9c44cab38 100644 --- a/examples/feign-example-cli/build.gradle +++ b/examples/feign-example-cli/build.gradle @@ -1,7 +1,7 @@ apply plugin: 'java' dependencies { - compile 'com.netflix.feign:feign-core:1.0.0' + compile 'com.netflix.feign:feign-core:1.1.1' compile 'com.google.code.gson:gson:2.2.4' provided 'com.squareup.dagger:dagger-compiler:1.0.1' } From 7e3def0521a09f81254616c84a1f33871d568c17 Mon Sep 17 00:00:00 2001 From: adriancole Date: Sat, 29 Jun 2013 15:05:29 -0700 Subject: [PATCH 053/179] to facilitate higher reuse, remove guava dep --- README.md | 4 +- build.gradle | 2 +- feign-core/src/main/java/feign/Client.java | 45 +++-- feign-core/src/main/java/feign/Contract.java | 63 +++--- feign-core/src/main/java/feign/Feign.java | 40 ++-- .../src/main/java/feign/FeignException.java | 10 +- .../src/main/java/feign/MethodHandler.java | 41 ++-- .../src/main/java/feign/MethodMetadata.java | 22 +- .../src/main/java/feign/ReflectiveFeign.java | 92 ++++----- feign-core/src/main/java/feign/Request.java | 53 ++--- .../src/main/java/feign/RequestTemplate.java | 188 +++++++++--------- feign-core/src/main/java/feign/Response.java | 86 ++++---- .../main/java/feign/RetryableException.java | 22 +- feign-core/src/main/java/feign/Retryer.java | 37 ++-- feign-core/src/main/java/feign/Target.java | 20 +- feign-core/src/main/java/feign/Util.java | 154 ++++++++++++++ feign-core/src/main/java/feign/Wire.java | 43 ++-- .../main/java/feign/codec/BodyEncoder.java | 17 +- .../src/main/java/feign/codec/Decoder.java | 40 ++-- .../src/main/java/feign/codec/Decoders.java | 59 ++++-- .../main/java/feign/codec/ErrorDecoder.java | 67 +++---- .../src/main/java/feign/codec/SAXDecoder.java | 11 +- .../java/feign/codec/ToStringDecoder.java | 41 +++- .../src/test/java/feign/ContractTest.java | 54 ++++- .../test/java/feign/DefaultRetryerTest.java | 29 +-- feign-core/src/test/java/feign/FeignTest.java | 6 +- .../test/java/feign/RequestTemplateTest.java | 4 +- .../feign/codec/DefaultErrorDecoderTest.java | 20 +- .../feign/codec/RetryAfterDecoderTest.java | 18 +- .../java/feign/examples/GitHubExample.java | 10 +- .../test/java/feign/examples/IAMExample.java | 10 +- .../src/main/java/feign/ribbon/LBClient.java | 17 +- .../feign/ribbon/LoadBalancingTarget.java | 2 +- .../feign/ribbon/LoadBalancingTargetTest.java | 2 +- .../java/feign/ribbon/RibbonClientTest.java | 2 +- 35 files changed, 771 insertions(+), 560 deletions(-) create mode 100644 feign-core/src/main/java/feign/Util.java diff --git a/README.md b/README.md index a370808e7e..bae3ef40f2 100644 --- a/README.md +++ b/README.md @@ -47,8 +47,8 @@ static class GsonModule { final Decoder gsonDecoder = new Decoder() { Gson gson = new Gson(); - @Override public Object decode(String methodKey, Reader reader, TypeToken type) { - return gson.fromJson(reader, type.getType()); + @Override public Object decode(String methodKey, Reader reader, Type type) { + return gson.fromJson(reader, type); } }; } diff --git a/build.gradle b/build.gradle index e811d22f15..d12dd2cd47 100644 --- a/build.gradle +++ b/build.gradle @@ -35,10 +35,10 @@ project(':feign-core') { } dependencies { - compile 'com.google.guava:guava:14.0.1' compile 'com.squareup.dagger:dagger:1.0.1' compile 'javax.ws.rs:jsr311-api:1.1.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' diff --git a/feign-core/src/main/java/feign/Client.java b/feign-core/src/main/java/feign/Client.java index 0fbee091ed..8a7b08cf3c 100644 --- a/feign-core/src/main/java/feign/Client.java +++ b/feign-core/src/main/java/feign/Client.java @@ -15,19 +15,19 @@ */ package feign; -import com.google.common.collect.ImmutableListMultimap; -import com.google.common.io.ByteSink; - import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; +import java.io.OutputStreamWriter; import java.io.Reader; +import java.io.Writer; import java.net.HttpURLConnection; import java.net.URL; +import java.util.Collection; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import javax.inject.Inject; import javax.net.ssl.HttpsURLConnection; @@ -36,8 +36,8 @@ import dagger.Lazy; import feign.Request.Options; -import static com.google.common.base.Charsets.UTF_8; -import static com.google.common.net.HttpHeaders.CONTENT_LENGTH; +import static feign.Util.CONTENT_LENGTH; +import static feign.Util.UTF_8; /** * Submits HTTP {@link Request requests}. Implementations are expected to be @@ -80,37 +80,46 @@ HttpURLConnection convertAndSend(Request request, Options options) throws IOExce connection.setRequestMethod(request.method()); Integer contentLength = null; - for (Entry header : request.headers().entries()) { - if (header.getKey().equals(CONTENT_LENGTH)) - contentLength = Integer.valueOf(header.getValue()); - connection.addRequestProperty(header.getKey(), header.getValue()); + for (String field : request.headers().keySet()) { + for (String value : request.headers().get(field)) { + if (field.equals(CONTENT_LENGTH)) { + contentLength = Integer.valueOf(value); + } + connection.addRequestProperty(field, value); + } } - if (request.body().isPresent()) { + if (request.body() != null) { if (contentLength != null) { connection.setFixedLengthStreamingMode(contentLength); } else { connection.setChunkedStreamingMode(8196); } connection.setDoOutput(true); - new ByteSink() { - public OutputStream openStream() throws IOException { - return connection.getOutputStream(); + OutputStream out = connection.getOutputStream(); + try { + out.write(request.body().getBytes(UTF_8)); + } finally { + try { + out.close(); + } catch (IOException suppressed) { // NOPMD } - }.asCharSink(UTF_8).write(request.body().get()); + } } return connection; } + private static final int BUF_SIZE = 0x800; // 2K chars (4K bytes) + Response convertResponse(HttpURLConnection connection) throws IOException { int status = connection.getResponseCode(); String reason = connection.getResponseMessage(); - ImmutableListMultimap.Builder headers = ImmutableListMultimap.builder(); + Map> headers = new LinkedHashMap>(); for (Map.Entry> field : connection.getHeaderFields().entrySet()) { // response message if (field.getKey() != null) - headers.putAll(field.getKey(), field.getValue()); + headers.put(field.getKey(), field.getValue()); } Integer length = connection.getContentLength(); @@ -123,7 +132,7 @@ Response convertResponse(HttpURLConnection connection) throws IOException { stream = connection.getInputStream(); } Reader body = stream != null ? new InputStreamReader(stream) : null; - return Response.create(status, reason, headers.build(), body, length); + return Response.create(status, reason, headers, body, length); } } } diff --git a/feign-core/src/main/java/feign/Contract.java b/feign-core/src/main/java/feign/Contract.java index f1db269c0f..031fa302df 100644 --- a/feign-core/src/main/java/feign/Contract.java +++ b/feign-core/src/main/java/feign/Contract.java @@ -15,14 +15,13 @@ */ package feign; -import com.google.common.base.Joiner; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import com.google.common.reflect.TypeToken; - import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; import javax.ws.rs.Consumes; import javax.ws.rs.FormParam; @@ -33,28 +32,29 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; -import static com.google.common.base.Preconditions.checkState; -import static com.google.common.net.HttpHeaders.ACCEPT; -import static com.google.common.net.HttpHeaders.CONTENT_TYPE; +import static feign.Util.ACCEPT; +import static feign.Util.CONTENT_TYPE; +import static feign.Util.checkState; +import static feign.Util.join; /** * Defines what annotations and values are valid on interfaces. */ public final class Contract { - public static ImmutableSet parseAndValidatateMetadata(Class declaring) { - ImmutableSet.Builder builder = ImmutableSet.builder(); + public static Set parseAndValidatateMetadata(Class declaring) { + Set metadata = new LinkedHashSet(); for (Method method : declaring.getDeclaredMethods()) { if (method.getDeclaringClass() == Object.class) continue; - builder.add(parseAndValidatateMetadata(method)); + metadata.add(parseAndValidatateMetadata(method)); } - return builder.build(); + return metadata; } public static MethodMetadata parseAndValidatateMetadata(Method method) { MethodMetadata data = new MethodMetadata(); - data.returnType(TypeToken.of(method.getGenericReturnType())); + data.returnType(method.getGenericReturnType()); data.configKey(Feign.configKey(method)); for (Annotation methodAnnotation : method.getAnnotations()) { @@ -75,9 +75,9 @@ public static MethodMetadata parseAndValidatateMetadata(Method method) { } else if (annotationType == Path.class) { data.template().append(Path.class.cast(methodAnnotation).value()); } else if (annotationType == Produces.class) { - data.template().header(CONTENT_TYPE, Joiner.on(',').join(((Produces) methodAnnotation).value())); + data.template().header(CONTENT_TYPE, join(',', ((Produces) methodAnnotation).value())); } else if (annotationType == Consumes.class) { - data.template().header(ACCEPT, Joiner.on(',').join(((Consumes) methodAnnotation).value())); + data.template().header(ACCEPT, join(',', ((Consumes) methodAnnotation).value())); } } checkState(data.template().method() != null, "Method %s not annotated with HTTP method type (ex. GET, POST)", @@ -95,28 +95,24 @@ public static MethodMetadata parseAndValidatateMetadata(Method method) { for (Annotation parameterAnnotation : parameterAnnotations) { Class annotationType = parameterAnnotation.annotationType(); if (annotationType == PathParam.class) { - data.indexToName().put(i, PathParam.class.cast(parameterAnnotation).value()); + indexName(data, i, PathParam.class.cast(parameterAnnotation).value()); hasHttpAnnotation = true; } else if (annotationType == QueryParam.class) { String name = QueryParam.class.cast(parameterAnnotation).value(); - data.template().query( - name, - ImmutableList.builder().addAll(data.template().queries().get(name)) - .add(String.format("{%s}", name)).build()); - data.indexToName().put(i, name); + Collection query = addTemplatedParam(data.template().queries().get(name), name); + data.template().query(name, query); + indexName(data, i, name); hasHttpAnnotation = true; } else if (annotationType == HeaderParam.class) { String name = HeaderParam.class.cast(parameterAnnotation).value(); - data.template().header( - name, - ImmutableList.builder().addAll(data.template().headers().get(name)) - .add(String.format("{%s}", name)).build()); - data.indexToName().put(i, name); + Collection header = addTemplatedParam(data.template().headers().get(name), name); + data.template().header(name, header); + indexName(data, i, name); hasHttpAnnotation = true; } else if (annotationType == FormParam.class) { String form = FormParam.class.cast(parameterAnnotation).value(); data.formParams().add(form); - data.indexToName().put(i, form); + indexName(data, i, form); hasHttpAnnotation = true; } } @@ -132,4 +128,17 @@ public static MethodMetadata parseAndValidatateMetadata(Method method) { } return data; } + + private static Collection addTemplatedParam(Collection possiblyNull, String name) { + if (possiblyNull == null) + possiblyNull = new ArrayList(); + possiblyNull.add(String.format("{%s}", name)); + return possiblyNull; + } + + private static void indexName(MethodMetadata data, int i, String name) { + Collection names = data.indexToName().containsKey(i) ? data.indexToName().get(i) : new ArrayList(); + names.add(name); + data.indexToName().put(i, names); + } } diff --git a/feign-core/src/main/java/feign/Feign.java b/feign-core/src/main/java/feign/Feign.java index 9b8bd3252c..fe27c83b57 100644 --- a/feign-core/src/main/java/feign/Feign.java +++ b/feign-core/src/main/java/feign/Feign.java @@ -15,11 +15,10 @@ */ package feign; -import com.google.common.base.Optional; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; - import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Map; import javax.net.ssl.SSLSocketFactory; @@ -67,23 +66,16 @@ public static T create(Target target, Object... modules) { * {@link Target targeted} http apis. */ public static Feign create(Object... modules) { - Object[] modulesForGraph = ImmutableList.builder() // - .add(new Defaults()) // - .add(new ReflectiveFeign.Module()) // - .add(Optional.fromNullable(modules).or(new Object[]{})).build().toArray(); - return ObjectGraph.create(modulesForGraph).get(Feign.class); + return ObjectGraph.create(modulesForGraph(modules).toArray()).get(Feign.class); } + /** * Returns an {@link ObjectGraph Dagger ObjectGraph} that can inject a * {@link ReflectiveFeign reflective} Feign. */ public static ObjectGraph createObjectGraph(Object... modules) { - Object[] modulesForGraph = ImmutableList.builder() // - .add(new Defaults()) // - .add(new ReflectiveFeign.Module()) // - .add(Optional.fromNullable(modules).or(new Object[]{})).build().toArray(); - return ObjectGraph.create(modulesForGraph); + return ObjectGraph.create(modulesForGraph(modules).toArray()); } @dagger.Module(complete = false, injects = Feign.class, library = true) @@ -106,23 +98,23 @@ public static class Defaults { } @Provides Map noOptions() { - return ImmutableMap.of(); + return Collections.emptyMap(); } @Provides Map noBodyEncoders() { - return ImmutableMap.of(); + return Collections.emptyMap(); } @Provides Map noFormEncoders() { - return ImmutableMap.of(); + return Collections.emptyMap(); } @Provides Map noDecoders() { - return ImmutableMap.of(); + return Collections.emptyMap(); } @Provides Map noErrorDecoders() { - return ImmutableMap.of(); + return Collections.emptyMap(); } } @@ -157,6 +149,16 @@ public static String configKey(Method method) { return builder.append(')').toString(); } + private static List modulesForGraph(Object... modules) { + List modulesForGraph = new ArrayList(3); + modulesForGraph.add(new Defaults()); + modulesForGraph.add(new ReflectiveFeign.Module()); + if (modules != null) + for (Object module : modules) + modulesForGraph.add(module); + return modulesForGraph; + } + Feign() { } diff --git a/feign-core/src/main/java/feign/FeignException.java b/feign-core/src/main/java/feign/FeignException.java index 500c0af287..bb4c6e61e2 100644 --- a/feign-core/src/main/java/feign/FeignException.java +++ b/feign-core/src/main/java/feign/FeignException.java @@ -15,30 +15,26 @@ */ package feign; -import com.google.common.reflect.TypeToken; - import java.io.IOException; -import feign.codec.Decoder; import feign.codec.ToStringDecoder; import static java.lang.String.format; /** - * Origin exception type for all HttpApis. + * Origin exception type for all Http Apis. */ public class FeignException extends RuntimeException { static FeignException errorReading(Request request, Response response, IOException cause) { return new FeignException(format("%s %s %s", cause.getMessage(), request.method(), request.url(), 0), cause); } - private static final Decoder toString = new ToStringDecoder(); - private static final TypeToken stringToken = TypeToken.of(String.class); + private static final ToStringDecoder toString = new ToStringDecoder(); public static FeignException errorStatus(String methodKey, Response response) { String message = format("status %s reading %s", response.status(), methodKey); try { - Object body = toString.decode(methodKey, response, stringToken); + Object body = toString.decode(methodKey, response, String.class); if (body != null) { response = Response.create(response.status(), response.reason(), response.headers(), body.toString()); message += "; content:\n" + body; diff --git a/feign-core/src/main/java/feign/MethodHandler.java b/feign-core/src/main/java/feign/MethodHandler.java index e734dc11f8..0761b927e2 100644 --- a/feign-core/src/main/java/feign/MethodHandler.java +++ b/feign-core/src/main/java/feign/MethodHandler.java @@ -15,11 +15,8 @@ */ package feign; -import com.google.common.base.Function; -import com.google.common.collect.ImmutableList; -import com.google.common.reflect.TypeToken; - import java.io.IOException; +import java.lang.reflect.Type; import java.net.URI; import javax.inject.Inject; @@ -29,13 +26,21 @@ import feign.codec.Decoder; import feign.codec.ErrorDecoder; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.net.HttpHeaders.LOCATION; import static feign.FeignException.errorExecuting; import static feign.FeignException.errorReading; +import static feign.Util.LOCATION; +import static feign.Util.checkNotNull; +import static feign.Util.firstOrNull; final class MethodHandler { + /** + * Those using guava will implement as {@code Function}. + */ + static interface BuildTemplateFromArgs { + public RequestTemplate apply(Object[] argv); + } + static class Factory { private final Client client; @@ -49,7 +54,7 @@ static class Factory { } public MethodHandler create(Target target, MethodMetadata md, - Function buildTemplateFromArgs, Options options, Decoder decoder, ErrorDecoder errorDecoder) { + BuildTemplateFromArgs buildTemplateFromArgs, Options options, Decoder decoder, ErrorDecoder errorDecoder) { return new MethodHandler(target, client, retryer, wire, md, buildTemplateFromArgs, options, decoder, errorDecoder); } } @@ -60,7 +65,7 @@ public MethodHandler create(Target target, MethodMetadata md, private final Provider retryer; private final Wire wire; - private final Function buildTemplateFromArgs; + private final BuildTemplateFromArgs buildTemplateFromArgs; private final Options options; private final Decoder decoder; private final ErrorDecoder errorDecoder; @@ -68,7 +73,7 @@ public MethodHandler create(Target target, MethodMetadata md, // cannot inject wildcards in dagger @SuppressWarnings("rawtypes") private MethodHandler(Target target, Client client, Provider retryer, Wire wire, MethodMetadata metadata, - Function buildTemplateFromArgs, Options options, Decoder decoder, ErrorDecoder errorDecoder) { + BuildTemplateFromArgs buildTemplateFromArgs, Options options, Decoder decoder, ErrorDecoder errorDecoder) { this.target = checkNotNull(target, "target"); this.client = checkNotNull(client, "client for %s", target); this.retryer = checkNotNull(retryer, "retryer for %s", target); @@ -93,7 +98,7 @@ public Object invoke(Object[] argv) throws Throwable { } } - public Object executeAndDecode(String configKey, RequestTemplate template, TypeToken returnType) + public Object executeAndDecode(String configKey, RequestTemplate template, Type returnType) throws Throwable { // create the request from a mutable copy of the input template. Request request = target.apply(new RequestTemplate(template)); @@ -102,13 +107,13 @@ public Object executeAndDecode(String configKey, RequestTemplate template, TypeT try { response = wire.wireAndRebufferResponse(target, response); if (response.status() >= 200 && response.status() < 300) { - if (returnType.getRawType().equals(Response.class)) { + if (returnType.equals(Response.class)) { return response; - } else if (returnType.getRawType() == URI.class && !response.body().isPresent()) { - ImmutableList location = response.headers().get(LOCATION); - if (!location.isEmpty()) - return URI.create(location.get(0)); - } else if (returnType.getRawType() == void.class) { + } else if (returnType == URI.class && response.body() == null) { + String location = firstOrNull(response.headers(), LOCATION); + if (location != null) + return URI.create(location); + } else if (returnType == void.class) { return null; } return decoder.decode(configKey, response, returnType); @@ -124,9 +129,9 @@ public Object executeAndDecode(String configKey, RequestTemplate template, TypeT } private void ensureBodyClosed(Response response) { - if (response.body().isPresent()) { + if (response.body() != null) { try { - response.body().get().close(); + response.body().close(); } catch (IOException ignored) { // NOPMD } } diff --git a/feign-core/src/main/java/feign/MethodMetadata.java b/feign-core/src/main/java/feign/MethodMetadata.java index e9b02700a3..2b8bc4d4b3 100644 --- a/feign-core/src/main/java/feign/MethodMetadata.java +++ b/feign-core/src/main/java/feign/MethodMetadata.java @@ -15,25 +15,25 @@ */ package feign; -import com.google.common.collect.LinkedHashMultimap; -import com.google.common.collect.Lists; -import com.google.common.collect.SetMultimap; -import com.google.common.reflect.TypeToken; - import java.io.Serializable; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; public final class MethodMetadata implements Serializable { MethodMetadata() { } private String configKey; - private transient TypeToken returnType; + private transient Type returnType; private Integer urlIndex; private Integer bodyIndex; private RequestTemplate template = new RequestTemplate(); - private List formParams = Lists.newArrayList(); - private SetMultimap indexToName = LinkedHashMultimap.create(); + private List formParams = new ArrayList(); + private Map> indexToName = new LinkedHashMap>(); /** * @see Feign#configKey(java.lang.reflect.Method) @@ -47,11 +47,11 @@ MethodMetadata configKey(String configKey) { return this; } - public TypeToken returnType() { + public Type returnType() { return returnType; } - MethodMetadata returnType(TypeToken returnType) { + MethodMetadata returnType(Type returnType) { this.returnType = returnType; return this; } @@ -82,7 +82,7 @@ public List formParams() { return formParams; } - public SetMultimap indexToName() { + public Map> indexToName() { return indexToName; } diff --git a/feign-core/src/main/java/feign/ReflectiveFeign.java b/feign-core/src/main/java/feign/ReflectiveFeign.java index 5936cd5627..2bd0beea77 100644 --- a/feign-core/src/main/java/feign/ReflectiveFeign.java +++ b/feign-core/src/main/java/feign/ReflectiveFeign.java @@ -15,17 +15,11 @@ */ package feign; -import com.google.common.base.Function; -import com.google.common.base.Objects; -import com.google.common.base.Predicates; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableMap.Builder; -import com.google.common.collect.Maps; -import com.google.common.reflect.AbstractInvocationHandler; -import com.google.common.reflect.Reflection; - +import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; +import java.lang.reflect.Proxy; import java.util.Collection; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Set; @@ -41,17 +35,17 @@ import feign.codec.FormEncoder; import feign.codec.ToStringDecoder; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; import static feign.Contract.parseAndValidatateMetadata; +import static feign.Util.checkArgument; +import static feign.Util.checkNotNull; import static java.lang.String.format; @SuppressWarnings("rawtypes") public class ReflectiveFeign extends Feign { - private final Function> targetToHandlersByName; + private final ParseHandlersByName targetToHandlersByName; - @Inject ReflectiveFeign(Function> targetToHandlersByName) { + @Inject ReflectiveFeign(ParseHandlersByName targetToHandlersByName) { this.targetToHandlersByName = targetToHandlersByName; } @@ -61,28 +55,27 @@ public class ReflectiveFeign extends Feign { */ @Override public T newInstance(Target target) { Map nameToHandler = targetToHandlersByName.apply(target); - Builder methodToHandler = ImmutableMap.builder(); + Map methodToHandler = new LinkedHashMap(); for (Method method : target.type().getDeclaredMethods()) { if (method.getDeclaringClass() == Object.class) continue; methodToHandler.put(method, nameToHandler.get(Feign.configKey(method))); } - FeignInvocationHandler handler = new FeignInvocationHandler(target, methodToHandler.build()); - return Reflection.newProxy(target.type(), handler); + FeignInvocationHandler handler = new FeignInvocationHandler(target, methodToHandler); + return (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class[]{target.type()}, handler); } - static class FeignInvocationHandler extends AbstractInvocationHandler { + static class FeignInvocationHandler implements InvocationHandler { private final Target target; private final Map methodToHandler; - FeignInvocationHandler(Target target, ImmutableMap methodToHandler) { + FeignInvocationHandler(Target target, Map methodToHandler) { this.target = checkNotNull(target, "target"); this.methodToHandler = checkNotNull(methodToHandler, "methodToHandler for %s", target); } - @Override - protected Object handleInvocation(Object proxy, Method method, Object[] args) throws Throwable { + @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return methodToHandler.get(method).invoke(args); } @@ -100,7 +93,7 @@ protected Object handleInvocation(Object proxy, Method method, Object[] args) th } @Override public String toString() { - return Objects.toStringHelper("").add("name", target.name()).add("url", target.url()).toString(); + return "target(" + target + ")"; } } @@ -112,11 +105,6 @@ public static class Module { @Provides Feign provideFeign(ReflectiveFeign in) { return in; } - - @Provides - Function> targetToHandlersByName(ParseHandlersByName parseHandlersByName) { - return parseHandlersByName; - } } private static IllegalStateException noConfig(String configKey, Class type) { @@ -124,7 +112,7 @@ private static IllegalStateException noConfig(String configKey, Class type) { type.getSimpleName())); } - static final class ParseHandlersByName implements Function> { + static final class ParseHandlersByName { private final Map options; private final Map bodyEncoders; private final Map formEncoders; @@ -143,9 +131,9 @@ static final class ParseHandlersByName implements Function apply(Target key) { + public Map apply(Target key) { Set metadata = parseAndValidatateMetadata(key.type()); - ImmutableMap.Builder builder = ImmutableMap.builder(); + Map result = new LinkedHashMap(); for (MethodMetadata md : metadata) { Options options = forMethodOrClass(this.options, md.configKey()); if (options == null) { @@ -153,7 +141,7 @@ static final class ParseHandlersByName implements Function buildTemplateFromArgs; - if (!md.formParams().isEmpty() && !md.template().bodyTemplate().isPresent()) { + BuildTemplateByResolvingArgs BuildTemplateByResolvingArgs; + if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) { FormEncoder formEncoder = forMethodOrClass(formEncoders, md.configKey()); if (formEncoder == null) { throw noConfig(md.configKey(), FormEncoder.class); } - buildTemplateFromArgs = new BuildFormEncodedTemplateFromArgs(md, formEncoder); + BuildTemplateByResolvingArgs = new BuildFormEncodedTemplateFromArgs(md, formEncoder); } else if (md.bodyIndex() != null) { BodyEncoder bodyEncoder = forMethodOrClass(bodyEncoders, md.configKey()); if (bodyEncoder == null) { throw noConfig(md.configKey(), BodyEncoder.class); } - buildTemplateFromArgs = new BuildBodyEncodedTemplateFromArgs(md, bodyEncoder); + BuildTemplateByResolvingArgs = new BuildBodyEncodedTemplateFromArgs(md, bodyEncoder); } else { - buildTemplateFromArgs = new BuildTemplateFromArgs(md); + BuildTemplateByResolvingArgs = new BuildTemplateByResolvingArgs(md); } - builder.put(md.configKey(), - factory.create(key, md, buildTemplateFromArgs, options, decoder, errorDecoder)); + result.put(md.configKey(), + factory.create(key, md, BuildTemplateByResolvingArgs, options, decoder, errorDecoder)); } - return builder.build(); + return result; } } - private static class BuildTemplateFromArgs implements Function { + private static class BuildTemplateByResolvingArgs implements MethodHandler.BuildTemplateFromArgs { protected final MethodMetadata metadata; - private BuildTemplateFromArgs(MethodMetadata metadata) { + private BuildTemplateByResolvingArgs(MethodMetadata metadata) { this.metadata = metadata; } - @Override public RequestTemplate apply(Object[] argv) { RequestTemplate mutable = new RequestTemplate(metadata.template()); if (metadata.urlIndex() != null) { @@ -201,23 +188,23 @@ public RequestTemplate apply(Object[] argv) { checkArgument(argv[urlIndex] != null, "URI parameter %s was null", urlIndex); mutable.insert(0, String.valueOf(argv[urlIndex])); } - ImmutableMap.Builder varBuilder = ImmutableMap.builder(); - for (Entry> entry : metadata.indexToName().asMap().entrySet()) { + Map varBuilder = new LinkedHashMap(); + for (Entry> entry : metadata.indexToName().entrySet()) { Object value = argv[entry.getKey()]; if (value != null) { // Null values are skipped. for (String name : entry.getValue()) varBuilder.put(name, value); } } - return resolve(argv, mutable, varBuilder.build()); + return resolve(argv, mutable, varBuilder); } - protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, ImmutableMap variables) { + protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map variables) { return mutable.resolve(variables); } } - private static class BuildFormEncodedTemplateFromArgs extends BuildTemplateFromArgs { + private static class BuildFormEncodedTemplateFromArgs extends BuildTemplateByResolvingArgs { private final FormEncoder formEncoder; private BuildFormEncodedTemplateFromArgs(MethodMetadata metadata, FormEncoder formEncoder) { @@ -226,13 +213,18 @@ private BuildFormEncodedTemplateFromArgs(MethodMetadata metadata, FormEncoder fo } @Override - protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, ImmutableMap variables) { - formEncoder.encodeForm(Maps.filterKeys(variables, Predicates.in(metadata.formParams())), mutable); + protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map variables) { + Map formVariables = new LinkedHashMap(); + for (Entry entry : variables.entrySet()) { + if (metadata.formParams().contains(entry.getKey())) + formVariables.put(entry.getKey(), entry.getValue()); + } + formEncoder.encodeForm(formVariables, mutable); return super.resolve(argv, mutable, variables); } } - private static class BuildBodyEncodedTemplateFromArgs extends BuildTemplateFromArgs { + private static class BuildBodyEncodedTemplateFromArgs extends BuildTemplateByResolvingArgs { private final BodyEncoder bodyEncoder; private BuildBodyEncodedTemplateFromArgs(MethodMetadata metadata, BodyEncoder bodyEncoder) { @@ -241,7 +233,7 @@ private BuildBodyEncodedTemplateFromArgs(MethodMetadata metadata, BodyEncoder bo } @Override - protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, ImmutableMap variables) { + protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map variables) { Object body = argv[metadata.bodyIndex()]; checkArgument(body != null, "Body parameter %s was null", metadata.bodyIndex()); bodyEncoder.encodeBody(body, mutable); diff --git a/feign-core/src/main/java/feign/Request.java b/feign-core/src/main/java/feign/Request.java index eb062c8c23..3df1613c45 100644 --- a/feign-core/src/main/java/feign/Request.java +++ b/feign-core/src/main/java/feign/Request.java @@ -15,14 +15,13 @@ */ package feign; -import com.google.common.base.Objects; -import com.google.common.base.Optional; -import com.google.common.collect.ImmutableListMultimap; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; -import java.util.Map.Entry; - -import static com.google.common.base.Objects.equal; -import static com.google.common.base.Preconditions.checkNotNull; +import static feign.Util.checkNotNull; +import static feign.Util.valuesOrEmpty; /** * An immutable request to an http server. @@ -36,14 +35,16 @@ public final class Request { private final String method; private final String url; - private final ImmutableListMultimap headers; - private final Optional body; + private final Map> headers; + private final String body; - Request(String method, String url, ImmutableListMultimap headers, Optional body) { + Request(String method, String url, Map> headers, String body) { this.method = checkNotNull(method, "method of %s", url); this.url = checkNotNull(url, "url"); - this.headers = checkNotNull(headers, "headers of %s %s", method, url); - this.body = checkNotNull(body, "body of %s %s", method, url); + LinkedHashMap> copyOf = new LinkedHashMap>(); + copyOf.putAll(checkNotNull(headers, "headers of %s %s", method, url)); + this.headers = Collections.unmodifiableMap(copyOf); + this.body = body; // nullable } /* Method to invoke on the server. */ @@ -57,12 +58,12 @@ public String url() { } /* Ordered list of headers that will be sent to the server. */ - public ImmutableListMultimap headers() { + public Map> headers() { return headers; } /* If present, this is the replayable body to send to the server. */ - public Optional body() { + public String body() { return body; } @@ -100,28 +101,16 @@ public int readTimeoutMillis() { } } - @Override public int hashCode() { - return Objects.hashCode(method, url, headers, body); - } - - @Override public boolean equals(Object obj) { - if (this == obj) - return true; - if (Request.class != obj.getClass()) - return false; - Request that = Request.class.cast(obj); - return equal(this.method, that.method) && equal(this.url, that.url) && equal(this.headers, that.headers) - && equal(this.body, that.body); - } - @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append(method).append(' ').append(url).append(" HTTP/1.1\n"); - for (Entry header : headers.entries()) { - builder.append(header.getKey()).append(": ").append(header.getValue()).append('\n'); + for (String field : headers.keySet()) { + for (String value : valuesOrEmpty(headers, field)) { + builder.append(field).append(": ").append(value).append('\n'); + } } - if (body.isPresent()) { - builder.append('\n').append(body.get()); + if (body != null) { + builder.append('\n').append(body); } return builder.toString(); } diff --git a/feign-core/src/main/java/feign/RequestTemplate.java b/feign-core/src/main/java/feign/RequestTemplate.java index b2b1d9adb5..9dfe51e238 100644 --- a/feign-core/src/main/java/feign/RequestTemplate.java +++ b/feign-core/src/main/java/feign/RequestTemplate.java @@ -15,34 +15,26 @@ */ package feign; -import com.google.common.base.Objects; -import com.google.common.base.Optional; -import com.google.common.base.Splitter; -import com.google.common.base.Strings; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableListMultimap; -import com.google.common.collect.Iterables; -import com.google.common.collect.LinkedListMultimap; -import com.google.common.collect.ListMultimap; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.google.common.collect.Multimap; -import com.google.common.collect.Multimaps; - import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.net.URLDecoder; import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; -import static com.google.common.base.Charsets.UTF_8; -import static com.google.common.base.Objects.equal; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.net.HttpHeaders.CONTENT_LENGTH; +import static feign.Util.CONTENT_LENGTH; +import static feign.Util.UTF_8; +import static feign.Util.checkNotNull; +import static feign.Util.emptyToNull; +import static feign.Util.toArray; +import static feign.Util.valuesOrEmpty; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; @@ -82,10 +74,10 @@ public final class RequestTemplate implements Serializable { private String method; /* final to encourage mutable use vs replacing the object. */ private StringBuilder url = new StringBuilder(); - private final ListMultimap queries = LinkedListMultimap.create(); - private final ListMultimap headers = LinkedListMultimap.create(); - private Optional body = Optional.absent(); - private Optional bodyTemplate = Optional.absent(); + private final Map> queries = new LinkedHashMap>(); + private final Map> headers = new LinkedHashMap>(); + private String body; + private String bodyTemplate; public RequestTemplate() { @@ -125,7 +117,7 @@ public RequestTemplate(RequestTemplate toCopy) { * just the URL */ public RequestTemplate resolve(Map unencoded) { - Map encoded = Maps.newLinkedHashMap(); + Map encoded = new LinkedHashMap(); for (Entry entry : unencoded.entrySet()) { encoded.put(entry.getKey(), urlEncode(String.valueOf(entry.getValue()))); } @@ -135,28 +127,32 @@ public RequestTemplate resolve(Map unencoded) { String resolvedUrl = expand(url.toString(), encoded).replace("%2F", "/"); url = new StringBuilder(resolvedUrl); - ListMultimap resolvedHeaders = LinkedListMultimap.create(); - for (Entry entry : headers.entries()) { - String value = null; - if (entry.getValue().indexOf('{') == 0) { - value = String.valueOf(unencoded.get(entry.getKey())); - } else { - value = entry.getValue(); + Map> resolvedHeaders = new LinkedHashMap>(); + for (String field : headers.keySet()) { + Collection resolvedValues = new ArrayList(); + for (String value : valuesOrEmpty(headers, field)) { + String resolved; + if (value.indexOf('{') == 0) { + resolved = String.valueOf(unencoded.get(field)); + } else { + resolved = value; + } + if (resolved != null) + resolvedValues.add(resolved); } - if (value != null) - resolvedHeaders.put(entry.getKey(), value); + resolvedHeaders.put(field, resolvedValues); } headers.clear(); headers.putAll(resolvedHeaders); - if (bodyTemplate.isPresent()) - body(urlDecode(expand(bodyTemplate.get(), unencoded))); + if (bodyTemplate != null) + body(urlDecode(expand(bodyTemplate, unencoded))); return this; } /* roughly analogous to {@code javax.ws.rs.client.Target.request()}. */ public Request request() { return new Request(method, new StringBuilder(url).append(queryLine()).toString(), - ImmutableListMultimap.copyOf(headers), body); + headers, body); } private static String urlDecode(String arg) { @@ -197,7 +193,7 @@ public static String expand(String template, Map variables) { boolean inVar = false; StringBuilder var = new StringBuilder(); StringBuilder builder = new StringBuilder(); - for (char c : Lists.charactersOf(template)) { + for (char c : template.toCharArray()) { switch (c) { case '{': inVar = true; @@ -274,10 +270,13 @@ public String url() { * @see #queries() */ public RequestTemplate query(String configKey, String... values) { - queries.removeAll(checkNotNull(configKey, "configKey")); + queries.remove(checkNotNull(configKey, "configKey")); if (values != null && values.length > 0 && values[0] != null) { - for (String value : values) - this.queries.put(encodeIfNotVariable(configKey), encodeIfNotVariable(value)); + ArrayList encoded = new ArrayList(); + for (String value : values) { + encoded.add(encodeIfNotVariable(value)); + } + this.queries.put(encodeIfNotVariable(configKey), encoded); } return this; } @@ -285,7 +284,7 @@ public RequestTemplate query(String configKey, String... values) { /* @see #query(String, String...) */ public RequestTemplate query(String configKey, Iterable values) { if (values != null) - return query(configKey, Iterables.toArray(values, String.class)); + return query(configKey, toArray(values, String.class)); return query(configKey, (String[]) null); } @@ -313,12 +312,12 @@ private String encodeIfNotVariable(String in) { * with. * @see #queries() */ - public RequestTemplate queries(Multimap queries) { + public RequestTemplate queries(Map> queries) { if (queries == null || queries.isEmpty()) { this.queries.clear(); } else { - for (Entry> entry : queries.asMap().entrySet()) - query(entry.getKey(), Iterables.toArray(entry.getValue(), String.class)); + for (Entry> entry : queries.entrySet()) + query(entry.getKey(), toArray(entry.getValue(), String.class)); } return this; } @@ -328,11 +327,20 @@ public RequestTemplate queries(Multimap queries) { * * @see Request#url() */ - public ListMultimap queries() { - ListMultimap unencoded = LinkedListMultimap.create(); - for (Entry entry : queries.entries()) - unencoded.put(urlDecode(entry.getKey()), urlDecode(entry.getValue())); - return Multimaps.unmodifiableListMultimap(unencoded); + public Map> queries() { + Map> decoded = new LinkedHashMap>(); + for (String field : queries.keySet()) { + Collection decodedValues = new ArrayList(); + for (String value : valuesOrEmpty(queries, field)) { + if (value != null) { + decodedValues.add(urlDecode(value)); + } else { + decodedValues.add(null); + } + } + decoded.put(urlDecode(field), decodedValues); + } + return Collections.unmodifiableMap(decoded); } /** @@ -361,16 +369,16 @@ public ListMultimap queries() { public RequestTemplate header(String configKey, String... values) { checkNotNull(configKey, "header configKey"); if (values == null || (values.length == 1 && values[0] == null)) - headers.removeAll(configKey); + headers.remove(configKey); else - this.headers.replaceValues(configKey, ImmutableList.copyOf(values)); + this.headers.put(configKey, Arrays.asList(values)); return this; } /* @see #header(String, String...) */ public RequestTemplate header(String configKey, Iterable values) { if (values != null) - return header(configKey, Iterables.toArray(values, String.class)); + return header(configKey, toArray(values, String.class)); return header(configKey, (String[]) null); } @@ -392,7 +400,7 @@ public RequestTemplate header(String configKey, Iterable values) { * with. * @see #headers() */ - public RequestTemplate headers(Multimap headers) { + public RequestTemplate headers(Map> headers) { if (headers == null || headers.isEmpty()) this.headers.clear(); else @@ -405,29 +413,29 @@ public RequestTemplate headers(Multimap headers) { * * @see Request#headers() */ - public ListMultimap headers() { - return ImmutableListMultimap.copyOf(headers); + public Map> headers() { + return Collections.unmodifiableMap(headers); } /** - * replaces the {@link com.google.common.net.HttpHeaders#CONTENT_LENGTH} header. + * replaces the {@link feign.Util#CONTENT_LENGTH} header. *

* Usually populated by {@link feign.codec.BodyEncoder} or {@link feign.codec.FormEncoder} * * @see Request#body() */ public RequestTemplate body(String body) { - this.body = Optional.fromNullable(body); - if (this.body.isPresent()) { + this.body = body; + if (this.body != null) { byte[] contentLength = body.getBytes(UTF_8); header(CONTENT_LENGTH, String.valueOf(contentLength.length)); } - this.bodyTemplate = Optional.absent(); + this.bodyTemplate = null; return this; } /* @see Request#body() */ - public Optional body() { + public String body() { return body; } @@ -437,8 +445,8 @@ public Optional body() { * @see Request#body() */ public RequestTemplate bodyTemplate(String bodyTemplate) { - this.bodyTemplate = Optional.fromNullable(bodyTemplate); - this.body = Optional.absent(); + this.bodyTemplate = bodyTemplate; + this.body = null; return this; } @@ -446,14 +454,10 @@ public RequestTemplate bodyTemplate(String bodyTemplate) { * @see Request#body() * @see #expand(String, Map) */ - public Optional bodyTemplate() { + public String bodyTemplate() { return bodyTemplate; } - @Override public int hashCode() { - return Objects.hashCode(method, url, queries, headers, body); - } - /** * if there are any query params in the {@link #body()}, this will extract * them out. @@ -465,7 +469,7 @@ private StringBuilder pullAnyQueriesOutOfUrl(StringBuilder url) { int queryIndex = url.indexOf("?"); if (queryIndex != -1) { String queryLine = url.substring(queryIndex + 1); - ListMultimap firstQueries = parseAndDecodeQueries(queryLine); + Map> firstQueries = parseAndDecodeQueries(queryLine); if (!queries.isEmpty()) { firstQueries.putAll(queries); queries.clear(); @@ -476,9 +480,9 @@ private StringBuilder pullAnyQueriesOutOfUrl(StringBuilder url) { return url; } - private static ListMultimap parseAndDecodeQueries(String queryLine) { - ListMultimap map = LinkedListMultimap.create(); - if (Strings.emptyToNull(queryLine) == null) + private static Map> parseAndDecodeQueries(String queryLine) { + Map> map = new LinkedHashMap>(); + if (emptyToNull(queryLine) == null) return map; if (queryLine.indexOf('&') == -1) { if (queryLine.indexOf('=') != -1) @@ -486,14 +490,21 @@ private static ListMultimap parseAndDecodeQueries(String queryLi else map.put(queryLine, null); } else { - for (String part : Splitter.on('&').split(queryLine)) { - putKV(part, map); + char[] chars = queryLine.toCharArray(); + int start = 0; + int i = 0; + for (; i < chars.length; i++) { + if (chars[i] == '&') { + putKV(queryLine.substring(start, i), map); + start = i + 1; + } } + putKV(queryLine.substring(start, i), map); } return map; } - private static void putKV(String stringToParse, Multimap map) { + private static void putKV(String stringToParse, Map> map) { String key; String value; // note that '=' can be a valid part of the value @@ -505,17 +516,9 @@ private static void putKV(String stringToParse, Multimap map) { key = urlDecode(stringToParse.substring(0, firstEq)); value = urlDecode(stringToParse.substring(firstEq + 1)); } - map.put(key, value); - } - - @Override public boolean equals(Object obj) { - if (this == obj) - return true; - if (RequestTemplate.class != obj.getClass()) - return false; - RequestTemplate that = RequestTemplate.class.cast(obj); - return equal(this.method, that.method) && equal(this.url, that.url) && equal(this.queries, that.queries) - && equal(this.headers, that.headers) && equal(this.body, that.body); + Collection values = map.containsKey(key) ? map.get(key) : new ArrayList(); + values.add(value); + map.put(key, values); } @Override public String toString() { @@ -526,18 +529,21 @@ public String queryLine() { if (queries.isEmpty()) return ""; StringBuilder queryBuilder = new StringBuilder(); - for (Entry pair : queries.entries()) { - queryBuilder.append('&'); - queryBuilder.append(pair.getKey()); - if (pair.getValue() != null) - queryBuilder.append('='); - if (pair.getValue() != null && !pair.getValue().equals("")) { - queryBuilder.append(pair.getValue()); + for (String field : queries.keySet()) { + for (String value : valuesOrEmpty(queries, field)) { + queryBuilder.append('&'); + queryBuilder.append(field); + if (value != null) { + queryBuilder.append('='); + if (!value.isEmpty()) + queryBuilder.append(value); + } } } queryBuilder.deleteCharAt(0); return queryBuilder.insert(0, '?').toString(); } + private static final long serialVersionUID = 1L; } diff --git a/feign-core/src/main/java/feign/Response.java b/feign-core/src/main/java/feign/Response.java index 2fa628c239..2380010655 100644 --- a/feign-core/src/main/java/feign/Response.java +++ b/feign-core/src/main/java/feign/Response.java @@ -15,20 +15,19 @@ */ package feign; -import com.google.common.base.Objects; -import com.google.common.base.Optional; -import com.google.common.collect.ImmutableListMultimap; - import java.io.Closeable; import java.io.IOException; import java.io.Reader; import java.io.StringReader; -import java.util.Map.Entry; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; -import static com.google.common.base.Charsets.UTF_8; -import static com.google.common.base.Objects.equal; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; +import static feign.Util.UTF_8; +import static feign.Util.checkNotNull; +import static feign.Util.checkState; +import static feign.Util.valuesOrEmpty; /** * An immutable response to an http invocation which only returns string @@ -37,24 +36,26 @@ public final class Response { private final int status; private final String reason; - private final ImmutableListMultimap headers; - private final Optional body; + private final Map> headers; + private final Body body; - public static Response create(int status, String reason, ImmutableListMultimap headers, + public static Response create(int status, String reason, Map> headers, Reader chars, Integer length) { - return new Response(status, reason, headers, Optional.fromNullable(ReaderBody.orNull(chars, length))); + return new Response(status, reason, headers, ReaderBody.orNull(chars, length)); } - public static Response create(int status, String reason, ImmutableListMultimap headers, String chars) { - return new Response(status, reason, headers, Optional.fromNullable(StringBody.orNull(chars))); + public static Response create(int status, String reason, Map> headers, String chars) { + return new Response(status, reason, headers, StringBody.orNull(chars)); } - private Response(int status, String reason, ImmutableListMultimap headers, Optional body) { + private Response(int status, String reason, Map> headers, Body body) { checkState(status >= 200, "Invalid status code: %s", status); this.status = status; this.reason = checkNotNull(reason, "reason"); - this.headers = checkNotNull(headers, "headers"); - this.body = checkNotNull(body, "body"); + LinkedHashMap> copyOf = new LinkedHashMap>(); + copyOf.putAll(checkNotNull(headers, "headers")); + this.headers = Collections.unmodifiableMap(copyOf); + this.body = body; //nullable } /** @@ -70,24 +71,27 @@ public String reason() { return reason; } - public ImmutableListMultimap headers() { + public Map> headers() { return headers; } - public Optional body() { + /** + * if present, the response had a body + */ + public Body body() { return body; } public interface Body extends Closeable { /** - * length in bytes, if known. + * length in bytes, if known. Null if not. *

*

Note

This is an integer as most implementations cannot do * bodies > 2GB. Moreover, the scope of this interface doesn't include * large bodies. */ - Optional length(); + Integer length(); /** * True if {@link #asReader()} can be called more than once. @@ -104,18 +108,18 @@ private static final class ReaderBody implements Response.Body { private static Body orNull(Reader chars, Integer length) { if (chars == null) return null; - return new ReaderBody(chars, Optional.fromNullable(length)); + return new ReaderBody(chars, length); } private final Reader chars; - private final Optional length; + private final Integer length; - private ReaderBody(Reader chars, Optional length) { + private ReaderBody(Reader chars, Integer length) { this.chars = chars; this.length = length; } - @Override public Optional length() { + @Override public Integer length() { return length; } @@ -145,11 +149,11 @@ public StringBody(String chars) { this.chars = chars; } - private volatile Optional length; + private volatile Integer length; - @Override public Optional length() { + @Override public Integer length() { if (length == null) { - length = Optional.of(chars.getBytes(UTF_8).length); + length = chars.getBytes(UTF_8).length; } return length; } @@ -170,28 +174,16 @@ public String toString() { } } - @Override public int hashCode() { - return Objects.hashCode(status, reason, headers, body); - } - - @Override public boolean equals(Object obj) { - if (this == obj) - return true; - if (Response.class != obj.getClass()) - return false; - Response that = Response.class.cast(obj); - return equal(this.status, that.status) && equal(this.reason, that.reason) && equal(this.headers, that.headers) - && equal(this.body, that.body); - } - @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("HTTP/1.1 ").append(status).append(' ').append(reason).append('\n'); - for (Entry header : headers.entries()) { - builder.append(header.getKey()).append(": ").append(header.getValue()).append('\n'); + for (String field : headers.keySet()) { + for (String value : valuesOrEmpty(headers, field)) { + builder.append(field).append(": ").append(value).append('\n'); + } } - if (body.isPresent()) { - builder.append('\n').append(body.get()); + if (body != null) { + builder.append('\n').append(body); } return builder.toString(); } diff --git a/feign-core/src/main/java/feign/RetryableException.java b/feign-core/src/main/java/feign/RetryableException.java index 3b9d3065ea..f5d6eabb9b 100644 --- a/feign-core/src/main/java/feign/RetryableException.java +++ b/feign-core/src/main/java/feign/RetryableException.java @@ -15,8 +15,6 @@ */ package feign; -import com.google.common.base.Optional; - import java.util.Date; /** @@ -28,32 +26,32 @@ public class RetryableException extends FeignException { private static final long serialVersionUID = 1L; - private final Optional retryAfter; + private final Date retryAfter; /** - * @param retryAfter usually corresponds to the {@link com.google.common.net.HttpHeaders#RETRY_AFTER} - * header. + * @param retryAfter usually corresponds to the {@link feign.Util#RETRY_AFTER} + * header. */ public RetryableException(String message, Throwable cause, Date retryAfter) { super(message, cause); - this.retryAfter = Optional.fromNullable(retryAfter); + this.retryAfter = retryAfter; } /** - * @param retryAfter usually corresponds to the {@link com.google.common.net.HttpHeaders#RETRY_AFTER} - * header. + * @param retryAfter usually corresponds to the {@link feign.Util#RETRY_AFTER} + * header. */ public RetryableException(String message, Date retryAfter) { super(message); - this.retryAfter = Optional.fromNullable(retryAfter); + this.retryAfter = retryAfter; } /** - * Sometimes corresponds to the {@link com.google.common.net.HttpHeaders#RETRY_AFTER} header + * Sometimes corresponds to the {@link feign.Util#RETRY_AFTER} header * present in {@code 503} status. Other times parsed from an - * application-specific response. + * application-specific response. Null if unknown. */ - public Optional retryAfter() { + public Date retryAfter() { return retryAfter; } } diff --git a/feign-core/src/main/java/feign/Retryer.java b/feign-core/src/main/java/feign/Retryer.java index a18cd421e4..cad03c2877 100644 --- a/feign-core/src/main/java/feign/Retryer.java +++ b/feign-core/src/main/java/feign/Retryer.java @@ -15,12 +15,6 @@ */ package feign; -import com.google.common.base.Ticker; - -import static com.google.common.primitives.Longs.max; -import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly; -import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static java.util.concurrent.TimeUnit.NANOSECONDS; import static java.util.concurrent.TimeUnit.SECONDS; /** @@ -42,8 +36,16 @@ public static class Default implements Retryer { private final long period; private final long maxPeriod; + // visible for testing; + protected long currentTimeMillis() { + return System.currentTimeMillis(); + } + + int attempt; + long sleptForMillis; + public Default() { - this(MILLISECONDS.toNanos(100), SECONDS.toNanos(1), 5); + this(100, SECONDS.toMillis(1), 5); } public Default(long period, long maxPeriod, int maxAttempts) { @@ -53,23 +55,26 @@ public Default(long period, long maxPeriod, int maxAttempts) { this.attempt = 1; } - // visible for testing; - Ticker ticker = Ticker.systemTicker(); - int attempt; - long sleptForNanos; - public void continueOrPropagate(RetryableException e) { if (attempt++ >= maxAttempts) throw e; long interval; - if (e.retryAfter().isPresent()) { - interval = max(maxPeriod, e.retryAfter().get().getTime() - ticker.read(), 0); + if (e.retryAfter() != null) { + interval = e.retryAfter().getTime() - currentTimeMillis(); + if (interval > maxPeriod) + interval = maxPeriod; + if (interval < 0) + return; } else { interval = nextMaxInterval(); } - sleepUninterruptibly(interval, NANOSECONDS); - sleptForNanos += interval; + try { + Thread.sleep(interval); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + sleptForMillis += interval; } /** diff --git a/feign-core/src/main/java/feign/Target.java b/feign-core/src/main/java/feign/Target.java index 16f2aba444..70c5a7f367 100644 --- a/feign-core/src/main/java/feign/Target.java +++ b/feign-core/src/main/java/feign/Target.java @@ -15,12 +15,10 @@ */ package feign; -import com.google.common.base.Function; -import com.google.common.base.Objects; -import com.google.common.base.Strings; +import java.util.Arrays; -import static com.google.common.base.Objects.equal; -import static com.google.common.base.Preconditions.checkNotNull; +import static feign.Util.checkNotNull; +import static feign.Util.emptyToNull; /** *

relationship to JAXRS 2.0

@@ -30,7 +28,7 @@ * * @param type of the interface this target applies to. */ -public interface Target extends Function { +public interface Target { /* The type of the interface this target applies to. ex. {@code Route53}. */ Class type(); @@ -61,7 +59,7 @@ public interface Target extends Function { * except that we expect transient, but necessary decoration to be applied * on invocation. */ - @Override public Request apply(RequestTemplate input); + public Request apply(RequestTemplate input); public static class HardCodedTarget implements Target { private final Class type; @@ -74,8 +72,8 @@ public HardCodedTarget(Class type, String url) { public HardCodedTarget(Class type, String name, String url) { this.type = checkNotNull(type, "type"); - this.name = checkNotNull(Strings.emptyToNull(name), "name"); - this.url = checkNotNull(Strings.emptyToNull(url), "url"); + this.name = checkNotNull(emptyToNull(name), "name"); + this.url = checkNotNull(emptyToNull(url), "url"); } @Override public Class type() { @@ -98,7 +96,7 @@ public HardCodedTarget(Class type, String name, String url) { } @Override public int hashCode() { - return Objects.hashCode(type, name, url); + return Arrays.hashCode(new Object[]{type, name, url}); } @Override public boolean equals(Object obj) { @@ -107,7 +105,7 @@ public HardCodedTarget(Class type, String name, String url) { if (HardCodedTarget.class != obj.getClass()) return false; HardCodedTarget that = HardCodedTarget.class.cast(obj); - return equal(this.type, that.type) && equal(this.name, that.name) && equal(this.url, that.url); + return this.type.equals(that.type) && this.name.equals(that.name) && this.url.equals(that.url); } } } diff --git a/feign-core/src/main/java/feign/Util.java b/feign-core/src/main/java/feign/Util.java new file mode 100644 index 0000000000..c001aed24d --- /dev/null +++ b/feign-core/src/main/java/feign/Util.java @@ -0,0 +1,154 @@ +/* + * 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.lang.reflect.Array; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +import static java.lang.String.format; + +/** + * Utilities, typically copied in from guava, so as to avoid dependency conflicts. + */ +public class Util { + private Util() { // no instances + } + + // feign.Util + /** + * The HTTP Accept header field name. + */ + public static final String ACCEPT = "Accept"; + /** + * The HTTP Content-Length header field name. + */ + public static final String CONTENT_LENGTH = "Content-Length"; + /** + * The HTTP Content-Type header field name. + */ + public static final String CONTENT_TYPE = "Content-Type"; + /** + * The HTTP Host header field name. + */ + public static final String HOST = "Host"; + /** + * The HTTP Location header field name. + */ + public static final String LOCATION = "Location"; + /** + * The HTTP Retry-After header field name. + */ + public static final String RETRY_AFTER = "Retry-After"; + + // com.google.common.base.Charsets + /** + * UTF-8: eight-bit UCS Transformation Format. + */ + public static final Charset UTF_8 = Charset.forName("UTF-8"); + + /** + * Copy of {@code com.google.common.base.Preconditions#checkArgument}. + */ + public static void checkArgument(boolean expression, + String errorMessageTemplate, + Object... errorMessageArgs) { + if (!expression) { + throw new IllegalArgumentException( + format(errorMessageTemplate, errorMessageArgs)); + } + } + + /** + * Copy of {@code com.google.common.base.Preconditions#checkNotNull}. + */ + public static T checkNotNull(T reference, + String errorMessageTemplate, + Object... errorMessageArgs) { + if (reference == null) { + // If either of these parameters is null, the right thing happens anyway + throw new NullPointerException( + format(errorMessageTemplate, errorMessageArgs)); + } + return reference; + } + + /** + * Copy of {@code com.google.common.base.Preconditions#checkState}. + */ + public static void checkState(boolean expression, + String errorMessageTemplate, + Object... errorMessageArgs) { + if (!expression) { + throw new IllegalStateException( + format(errorMessageTemplate, errorMessageArgs)); + } + } + + /** + * Adapted from {@code com.google.common.base.Strings#emptyToNull}. + */ + public static String emptyToNull(String string) { + return string == null || string.isEmpty() ? null : string; + } + + public 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(); + } + + /** + * Adapted from {@code com.google.common.base.Strings#emptyToNull}. + */ + public static T[] toArray(Iterable iterable, Class type) { + Collection collection; + if (iterable instanceof Collection) { + collection = (Collection) iterable; + } else { + collection = new ArrayList(); + for (T element : iterable) { + collection.add(element); + } + } + T[] array = (T[]) Array.newInstance(type, collection.size()); + return collection.toArray(array); + } + + /** + * Returns an unmodifiable collection which may be empty, but is never null. + */ + public static Collection valuesOrEmpty(Map> map, String key) { + return map.containsKey(key) ? map.get(key) : Collections.emptyList(); + } + + public static T firstOrNull(Map> map, String key) { + if (map.containsKey(key) && !map.get(key).isEmpty()) { + return map.get(key).iterator().next(); + } + return null; + } +} diff --git a/feign-core/src/main/java/feign/Wire.java b/feign-core/src/main/java/feign/Wire.java index f63b517e15..2c054476b8 100644 --- a/feign-core/src/main/java/feign/Wire.java +++ b/feign-core/src/main/java/feign/Wire.java @@ -15,18 +15,18 @@ */ package feign; -import com.google.common.io.Closer; - import java.io.BufferedReader; import java.io.IOException; +import java.io.Reader; import java.text.SimpleDateFormat; -import java.util.Map.Entry; import java.util.logging.FileHandler; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; import java.util.logging.SimpleFormatter; +import static feign.Util.valuesOrEmpty; + /* Writes http headers and body. Plumb to your favorite log impl. */ public abstract class Wire { /* logs to the category {@link Wire} at {@link Level#FINE}. */ @@ -105,40 +105,43 @@ protected void log(Target target, String format, Object... args) { void wireRequest(Target target, Request request) { log(target, ">> %s %s HTTP/1.1", request.method(), request.url()); - - for (Entry header : request.headers().entries()) { - log(target, ">> %s: %s", header.getKey(), header.getValue()); + for (String field : request.headers().keySet()) { + for (String value : valuesOrEmpty(request.headers(), field)) { + log(target, ">> %s: %s", field, value); + } } - if (request.body().isPresent()) { + if (request.body() != null) { log(target, ">> "); // CRLF - log(target, ">> %s", request.body().get()); + log(target, ">> %s", request.body()); } } Response wireAndRebufferResponse(Target target, Response response) throws IOException { log(target, "<< HTTP/1.1 %s %s", response.status(), response.reason()); - - for (Entry header : response.headers().entries()) { - log(target, "<< %s: %s", header.getKey(), header.getValue()); + for (String field : response.headers().keySet()) { + for (String value : valuesOrEmpty(response.headers(), field)) { + log(target, "<< %s: %s", field, value); + } } - if (response.body().isPresent()) { + if (response.body() != null) { log(target, "<< "); // CRLF - Closer closer = Closer.create(); + Reader body = response.body().asReader(); try { - StringBuilder body = new StringBuilder(); - BufferedReader reader = new BufferedReader(closer.register(response.body().get().asReader())); + StringBuilder buffered = new StringBuilder(); + BufferedReader reader = new BufferedReader(body); String line; while ((line = reader.readLine()) != null) { - body.append(line); + buffered.append(line); log(target, "<< %s", line); } - return Response.create(response.status(), response.reason(), response.headers(), body.toString()); - } catch (Throwable e) { - throw closer.rethrow(e); + return Response.create(response.status(), response.reason(), response.headers(), buffered.toString()); } finally { - closer.close(); + try { + body.close(); + } catch (IOException suppressed) { // NOPMD + } } } return response; diff --git a/feign-core/src/main/java/feign/codec/BodyEncoder.java b/feign-core/src/main/java/feign/codec/BodyEncoder.java index 5ee03d5e60..6631d3e6a3 100644 --- a/feign-core/src/main/java/feign/codec/BodyEncoder.java +++ b/feign-core/src/main/java/feign/codec/BodyEncoder.java @@ -26,17 +26,16 @@ public interface BodyEncoder { *

*

    * public class GsonEncoder implements BodyEncoder {
-   *     private final Gson gson;
+   *   private final Gson gson;
    *
-   *     public GsonEncoder(Gson gson) {
-   *    this.gson = gson;
-   *     }
-   *
-   *     @Override
-   *     public void encodeBody(Object bodyParam, RequestTemplate base) {
-   *    base.body(gson.toJson(bodyParam));
-   *     }
+   *   public GsonEncoder(Gson gson) {
+   *     this.gson = gson;
+   *   }
    *
+   *   @Override
+   *   public void encodeBody(Object bodyParam, RequestTemplate base) {
+   *     base.body(gson.toJson(bodyParam));
+   *   }
    * }
    * 
*

diff --git a/feign-core/src/main/java/feign/codec/Decoder.java b/feign-core/src/main/java/feign/codec/Decoder.java index 3dbb910a17..9eac156daf 100644 --- a/feign-core/src/main/java/feign/codec/Decoder.java +++ b/feign-core/src/main/java/feign/codec/Decoder.java @@ -15,11 +15,9 @@ */ package feign.codec; -import com.google.common.io.Closer; -import com.google.common.reflect.TypeToken; - import java.io.IOException; import java.io.Reader; +import java.lang.reflect.Type; import feign.Response; @@ -31,16 +29,16 @@ *

*

  * public class GsonDecoder extends Decoder {
- *     private final Gson gson;
+ *   private final Gson gson;
  *
- *     public GsonDecoder(Gson gson) {
- *    this.gson = gson;
- *     }
+ *   public GsonDecoder(Gson gson) {
+ *     this.gson = gson;
+ *   }
  *
- *     @Override
- *     public Object decode(String methodKey, Reader reader, TypeToken<?> type) {
- *    return gson.fromJson(reader, type.getType());
- *     }
+ *   @Override
+ *   public Object decode(String methodKey, Reader reader, Type type) {
+ *     return gson.fromJson(reader, type);
+ *   }
  * }
  * 
*

@@ -67,20 +65,18 @@ public abstract class Decoder { * @return instance of {@code type} * @throws IOException if there was a network error reading the response. */ - public Object decode(String methodKey, Response response, TypeToken type) throws IOException { - Response.Body body = response.body().orNull(); + public Object decode(String methodKey, Response response, Type type) throws Throwable { + Response.Body body = response.body(); if (body == null) return null; - Closer closer = Closer.create(); + Reader reader = body.asReader(); try { - Reader reader = closer.register(body.asReader()); return decode(methodKey, reader, type); - } catch (IOException e) { - throw closer.rethrow(e, IOException.class); - } catch (Throwable e) { - throw closer.rethrow(e); } finally { - closer.close(); + try { + reader.close(); + } catch (IOException suppressed) { // NOPMD + } } } @@ -90,11 +86,11 @@ public Object decode(String methodKey, Response response, TypeToken type) thr * * @param methodKey {@link feign.Feign#configKey} of the java method that invoked the request. * ex. {@code IAM#getUser()} - * @param reader no need to close this, as {@link #decode(String, Response, TypeToken)} + * @param reader no need to close this, as {@link #decode(String, Response, Type)} * manages resources. * @param type Target object type. * @return instance of {@code type} * @throws Throwable will be propagated safely to the caller. */ - public abstract Object decode(String methodKey, Reader reader, TypeToken type) throws Throwable; + public abstract Object decode(String methodKey, Reader reader, Type type) throws Throwable; } diff --git a/feign-core/src/main/java/feign/codec/Decoders.java b/feign-core/src/main/java/feign/codec/Decoders.java index e8140b2d6e..56e85981b8 100644 --- a/feign-core/src/main/java/feign/codec/Decoders.java +++ b/feign-core/src/main/java/feign/codec/Decoders.java @@ -15,18 +15,14 @@ */ package feign.codec; -import com.google.common.base.Function; -import com.google.common.base.Functions; -import com.google.common.collect.ImmutableList; -import com.google.common.io.CharStreams; -import com.google.common.reflect.TypeToken; - import java.io.Reader; +import java.lang.reflect.Type; +import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; -import static com.google.common.base.Preconditions.checkNotNull; +import static feign.Util.checkNotNull; import static java.lang.String.format; import static java.util.regex.Pattern.DOTALL; import static java.util.regex.Pattern.compile; @@ -43,6 +39,17 @@ * facilitate these use cases. */ public class Decoders { + /** + * guava users will implement this with {@code ApplyFirstGroup}. + * + * @param intended result type + */ + public interface ApplyFirstGroup { + /** + * create a new instance from the non-null {@code firstGroup} specified. + */ + T apply(String firstGroup); + } /** * The first match group is applied to {@code applyGroups} and result @@ -54,13 +61,13 @@ public class Decoders { * decodeFirstDirPoolID = transformFirstGroup("<DirPoolID[ˆ>]*>([ˆ<]+)</DirPoolID>", ToLong.INSTANCE); * */ - public static Decoder transformFirstGroup(String pattern, final Function applyFirstGroup) { + public static Decoder transformFirstGroup(String pattern, final ApplyFirstGroup applyFirstGroup) { final Pattern patternForMatcher = compile(checkNotNull(pattern, "pattern"), DOTALL); checkNotNull(applyFirstGroup, "applyFirstGroup"); return new Decoder() { @Override - public Object decode(String methodKey, Reader reader, TypeToken type) throws Throwable { - Matcher matcher = patternForMatcher.matcher(CharStreams.toString(reader)); + public Object decode(String methodKey, Reader reader, Type type) throws Throwable { + Matcher matcher = patternForMatcher.matcher(Decoders.toString(reader)); if (matcher.find()) { return applyFirstGroup.apply(matcher.group(1)); } @@ -74,7 +81,7 @@ public Object decode(String methodKey, Reader reader, TypeToken type) throws } /** - * shortcut for {@link Decoders#transformFirstGroup(String, Function)} when + * shortcut for {@link Decoders#transformFirstGroup(String, ApplyFirstGroup)} when * {@code String} is the type you are decoding into. *

*

@@ -85,7 +92,7 @@ public Object decode(String methodKey, Reader reader, TypeToken type) throws * */ public static Decoder firstGroup(String pattern) { - return transformFirstGroup(pattern, Functions.identity()); + return transformFirstGroup(pattern, IDENTITY); } /** @@ -100,18 +107,18 @@ public static Decoder firstGroup(String pattern) { * decodeListOfZones = transformEachFirstGroup("/REST/Zone/([ˆ/]+)/", ToZone.INSTANCE); * */ - public static Decoder transformEachFirstGroup(String pattern, final Function applyFirstGroup) { + public static Decoder transformEachFirstGroup(String pattern, final ApplyFirstGroup applyFirstGroup) { final Pattern patternForMatcher = compile(checkNotNull(pattern, "pattern"), DOTALL); checkNotNull(applyFirstGroup, "applyFirstGroup"); return new Decoder() { @Override - public List decode(String methodKey, Reader reader, TypeToken type) throws Throwable { - Matcher matcher = patternForMatcher.matcher(CharStreams.toString(reader)); - ImmutableList.Builder builder = ImmutableList.builder(); + public List decode(String methodKey, Reader reader, Type type) throws Throwable { + Matcher matcher = patternForMatcher.matcher(Decoders.toString(reader)); + List result = new ArrayList(); while (matcher.find()) { - builder.add(applyFirstGroup.apply(matcher.group(1))); + result.add(applyFirstGroup.apply(matcher.group(1))); } - return builder.build(); + return result; } @Override public String toString() { @@ -122,7 +129,7 @@ public List decode(String methodKey, Reader reader, TypeToken type) throws } /** - * shortcut for {@link Decoders#transformEachFirstGroup(String, Function)} + * shortcut for {@link Decoders#transformEachFirstGroup(String, ApplyFirstGroup)} * when {@code List} is the type you are decoding into. *

* Ex. to pull a list zones names, which are http paths starting with @@ -133,6 +140,18 @@ public List decode(String methodKey, Reader reader, TypeToken type) throws * */ public static Decoder eachFirstGroup(String pattern) { - return transformEachFirstGroup(pattern, Functions.identity()); + return transformEachFirstGroup(pattern, IDENTITY); } + + private static String toString(Reader reader) throws Throwable { + return TO_STRING.decode(null, reader, null).toString(); + } + + private static final Decoder TO_STRING = new ToStringDecoder(); + + private static final ApplyFirstGroup IDENTITY = new ApplyFirstGroup() { + @Override public String apply(String firstGroup) { + return firstGroup; + } + }; } diff --git a/feign-core/src/main/java/feign/codec/ErrorDecoder.java b/feign-core/src/main/java/feign/codec/ErrorDecoder.java index 0ffd9cfbb9..e19c5f005d 100644 --- a/feign-core/src/main/java/feign/codec/ErrorDecoder.java +++ b/feign-core/src/main/java/feign/codec/ErrorDecoder.java @@ -15,11 +15,7 @@ */ package feign.codec; -import com.google.common.base.Function; -import com.google.common.base.Optional; -import com.google.common.base.Ticker; -import com.google.common.reflect.TypeToken; - +import java.lang.reflect.Type; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; @@ -29,10 +25,10 @@ import feign.Response; import feign.RetryableException; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.collect.Iterables.getFirst; -import static com.google.common.net.HttpHeaders.RETRY_AFTER; import static feign.FeignException.errorStatus; +import static feign.Util.RETRY_AFTER; +import static feign.Util.checkNotNull; +import static feign.Util.firstOrNull; import static java.util.Locale.US; import static java.util.concurrent.TimeUnit.NANOSECONDS; import static java.util.concurrent.TimeUnit.SECONDS; @@ -49,7 +45,7 @@ * class IllegalArgumentExceptionOn404Decoder extends ErrorDecoder { * * @Override - * public Object decode(String methodKey, Response response, TypeToken<?> type) throws Throwable { + * public Object decode(String methodKey, Response response, Type<?> type) throws Throwable { * if (response.status() == 404) * throw new IllegalArgumentException("zone not found"); * return ErrorDecoder.DEFAULT.decode(request, response, type); @@ -69,49 +65,51 @@ public interface ErrorDecoder { * * @param methodKey {@link feign.Feign#configKey} of the java method that invoked the request. ex. {@code IAM#getUser()} * @param response HTTP response where {@link Response#status() status} >= - * {@code 300}. + * {@code 300}. * @param type Target object type. * @return instance of {@code type} * @throws Throwable IOException, if there was a network error reading the - * response or an application-specific exception decoded by the - * implementation. If the throwable is retryable, it should be - * wrapped, or a subtype of {@link RetryableException} + * response or an application-specific exception decoded by the + * implementation. If the throwable is retryable, it should be + * wrapped, or a subtype of {@link RetryableException} */ - public Object decode(String methodKey, Response response, TypeToken type) throws Throwable; + public Object decode(String methodKey, Response response, Type type) throws Throwable; public static final ErrorDecoder DEFAULT = new ErrorDecoder() { private final RetryAfterDecoder retryAfterDecoder = new RetryAfterDecoder(); @Override - public Object decode(String methodKey, Response response, TypeToken type) throws Throwable { + public Object decode(String methodKey, Response response, Type type) throws Throwable { FeignException exception = errorStatus(methodKey, response); - Optional retryAfter = retryAfterDecoder.apply(getFirst(response.headers().get(RETRY_AFTER), null)); - if (retryAfter.isPresent()) - throw new RetryableException(exception.getMessage(), exception, retryAfter.get()); + Date retryAfter = retryAfterDecoder.apply(firstOrNull(response.headers(), RETRY_AFTER)); + if (retryAfter != null) + throw new RetryableException(exception.getMessage(), exception, retryAfter); throw exception; } }; /** - * Decodes a {@link com.google.common.net.HttpHeaders#RETRY_AFTER} header into an absolute date, + * Decodes a {@link feign.Util#RETRY_AFTER} header into an absolute date, * if possible. * * @see Retry-After - * format + * href="https://tools.ietf.org/html/rfc2616#section-14.37">Retry-After + * format */ - static class RetryAfterDecoder implements Function> { + static class RetryAfterDecoder { static final DateFormat RFC822_FORMAT = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", US); - private final Ticker currentTimeNanos; private final DateFormat rfc822Format; RetryAfterDecoder() { - this(Ticker.systemTicker(), RFC822_FORMAT); + this(RFC822_FORMAT); + } + + protected long currentTimeNanos() { + return System.currentTimeMillis(); } - RetryAfterDecoder(Ticker currentTimeNanos, DateFormat rfc822Format) { - this.currentTimeNanos = checkNotNull(currentTimeNanos, "currentTimeNanos"); + RetryAfterDecoder(DateFormat rfc822Format) { this.rfc822Format = checkNotNull(rfc822Format, "rfc822Format"); } @@ -120,23 +118,22 @@ static class RetryAfterDecoder implements Function> { * retried. * * @param retryAfter String in Retry-After format + * href="https://tools.ietf.org/html/rfc2616#section-14.37" + * >Retry-After format */ - @Override - public Optional apply(String retryAfter) { + public Date apply(String retryAfter) { if (retryAfter == null) - return Optional.absent(); + return null; if (retryAfter.matches("^[0-9]+$")) { - long currentTimeMillis = NANOSECONDS.toMillis(currentTimeNanos.read()); + long currentTimeMillis = NANOSECONDS.toMillis(currentTimeNanos()); long deltaMillis = SECONDS.toMillis(Long.parseLong(retryAfter)); - return Optional.of(new Date(currentTimeMillis + deltaMillis)); + return new Date(currentTimeMillis + deltaMillis); } synchronized (rfc822Format) { try { - return Optional.of(rfc822Format.parse(retryAfter)); + return rfc822Format.parse(retryAfter); } catch (ParseException ignored) { - return Optional.absent(); + return null; } } } diff --git a/feign-core/src/main/java/feign/codec/SAXDecoder.java b/feign-core/src/main/java/feign/codec/SAXDecoder.java index 5a36b2a13e..36a84171eb 100644 --- a/feign-core/src/main/java/feign/codec/SAXDecoder.java +++ b/feign-core/src/main/java/feign/codec/SAXDecoder.java @@ -15,8 +15,6 @@ */ package feign.codec; -import com.google.common.reflect.TypeToken; - import org.xml.sax.ContentHandler; import org.xml.sax.InputSource; import org.xml.sax.SAXException; @@ -24,12 +22,13 @@ import java.io.IOException; import java.io.Reader; +import java.lang.reflect.Type; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParserFactory; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; +import static feign.Util.checkNotNull; +import static feign.Util.checkState; public abstract class SAXDecoder extends Decoder { /* Implementations are not intended to be shared across requests. */ @@ -51,7 +50,7 @@ protected SAXDecoder(SAXParserFactory factory) { } @Override - public Object decode(String methodKey, Reader reader, TypeToken type) throws IOException, SAXException, + public Object decode(String methodKey, Reader reader, Type type) throws IOException, SAXException, ParserConfigurationException { ContentHandlerWithResult handler = typeToNewHandler(type); checkState(handler != null, "%s returned null for type %s", this, type); @@ -62,5 +61,5 @@ public Object decode(String methodKey, Reader reader, TypeToken type) throws return handler.getResult(); } - protected abstract ContentHandlerWithResult typeToNewHandler(TypeToken type); + protected abstract ContentHandlerWithResult typeToNewHandler(Type type); } diff --git a/feign-core/src/main/java/feign/codec/ToStringDecoder.java b/feign-core/src/main/java/feign/codec/ToStringDecoder.java index 72413d66e5..b1ca2ab55b 100644 --- a/feign-core/src/main/java/feign/codec/ToStringDecoder.java +++ b/feign-core/src/main/java/feign/codec/ToStringDecoder.java @@ -15,14 +15,45 @@ */ package feign.codec; -import com.google.common.io.CharStreams; -import com.google.common.reflect.TypeToken; - +import java.io.IOException; import java.io.Reader; +import java.lang.reflect.Type; +import java.nio.CharBuffer; + +import feign.Response; +/** + * Adapted from {@code com.google.common.io.CharStreams.toString()}. + */ public class ToStringDecoder extends Decoder { + private static final int BUF_SIZE = 0x800; // 2K chars (4K bytes) + + // overridden to throw only IOException + @Override + public Object decode(String methodKey, Response response, Type type) throws IOException { + Response.Body body = response.body(); + if (body == null) + return null; + Reader reader = body.asReader(); + try { + return decode(methodKey, reader, type); + } finally { + try { + reader.close(); + } catch (IOException suppressed) { // NOPMD + } + } + } + @Override - public Object decode(String methodKey, Reader reader, TypeToken type) throws Throwable { - return CharStreams.toString(reader); + public Object decode(String methodKey, Reader from, Type type) throws IOException { + StringBuilder to = new StringBuilder(); + CharBuffer buf = CharBuffer.allocate(BUF_SIZE); + while (from.read(buf) != -1) { + buf.flip(); + to.append(buf); + buf.clear(); + } + return to.toString(); } } diff --git a/feign-core/src/test/java/feign/ContractTest.java b/feign-core/src/test/java/feign/ContractTest.java index faec7ff527..164d7b78c3 100644 --- a/feign-core/src/test/java/feign/ContractTest.java +++ b/feign-core/src/test/java/feign/ContractTest.java @@ -34,8 +34,8 @@ import feign.RequestTemplate.Body; -import static com.google.common.net.HttpHeaders.CONTENT_TYPE; import static feign.Contract.parseAndValidatateMetadata; +import static feign.Util.CONTENT_TYPE; import static javax.ws.rs.HttpMethod.DELETE; import static javax.ws.rs.HttpMethod.GET; import static javax.ws.rs.HttpMethod.POST; @@ -70,14 +70,48 @@ interface Methods { } interface WithQueryParamsInPath { - @GET @Path("/?Action=GetUser&Version=2010-05-08") Response get(); + @GET @Path("/") Response none(); + + @GET @Path("/?Action=GetUser") Response one(); + + @GET @Path("/?Action=GetUser&Version=2010-05-08") Response two(); + + @GET @Path("/?Action=GetUser&Version=2010-05-08&limit=1") Response three(); + + @GET @Path("/?flag&Action=GetUser&Version=2010-05-08") Response empty(); } @Test public void queryParamsInPathExtract() throws Exception { - MethodMetadata md = parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("get")); - assertEquals(md.template().url(), "/"); - assertEquals(md.template().queries().get("Action"), ImmutableSet.of("GetUser")); - assertEquals(md.template().queries().get("Version"), ImmutableSet.of("2010-05-08")); + { + MethodMetadata md = parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("none")); + assertEquals(md.template().url(), "/"); + assertTrue(md.template().queries().isEmpty()); + } + { + MethodMetadata md = parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("one")); + assertEquals(md.template().url(), "/"); + assertEquals(md.template().queries().get("Action"), ImmutableSet.of("GetUser")); + } + { + MethodMetadata md = parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("two")); + assertEquals(md.template().url(), "/"); + assertEquals(md.template().queries().get("Action"), ImmutableSet.of("GetUser")); + assertEquals(md.template().queries().get("Version"), ImmutableSet.of("2010-05-08")); + } + { + MethodMetadata md = parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("three")); + assertEquals(md.template().url(), "/"); + assertEquals(md.template().queries().get("Action"), ImmutableSet.of("GetUser")); + assertEquals(md.template().queries().get("Version"), ImmutableSet.of("2010-05-08")); + assertEquals(md.template().queries().get("limit"), ImmutableSet.of("1")); + } + { + MethodMetadata md = parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("empty")); + assertEquals(md.template().url(), "/"); + assertTrue(md.template().queries().containsKey("flag")); + assertEquals(md.template().queries().get("Action"), ImmutableSet.of("GetUser")); + assertEquals(md.template().queries().get("Version"), ImmutableSet.of("2010-05-08")); + } } interface BodyWithoutParameters { @@ -86,8 +120,8 @@ interface BodyWithoutParameters { @Test public void bodyWithoutParameters() throws Exception { MethodMetadata md = parseAndValidatateMetadata(BodyWithoutParameters.class.getDeclaredMethod("post")); - assertEquals(md.template().body().get(), ""); - assertFalse(md.template().bodyTemplate().isPresent()); + assertEquals(md.template().body(), ""); + assertFalse(md.template().bodyTemplate() != null); assertTrue(md.formParams().isEmpty()); assertTrue(md.indexToName().isEmpty()); } @@ -127,8 +161,8 @@ void login( MethodMetadata md = parseAndValidatateMetadata(FormParams.class.getDeclaredMethod("login", String.class, String.class, String.class)); - assertFalse(md.template().body().isPresent()); - assertEquals(md.template().bodyTemplate().get(), + assertFalse(md.template().body() != null); + assertEquals(md.template().bodyTemplate(), "%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D"); assertEquals(md.formParams(), ImmutableList.of("customer_name", "user_name", "password")); assertEquals(md.indexToName().get(0), ImmutableSet.of("customer_name")); diff --git a/feign-core/src/test/java/feign/DefaultRetryerTest.java b/feign-core/src/test/java/feign/DefaultRetryerTest.java index c36cca6dc7..6ccc9c6857 100644 --- a/feign-core/src/test/java/feign/DefaultRetryerTest.java +++ b/feign-core/src/test/java/feign/DefaultRetryerTest.java @@ -15,8 +15,6 @@ */ package feign; -import com.google.common.base.Ticker; - import org.testng.annotations.Test; import java.util.Date; @@ -33,42 +31,37 @@ public void only5TriesAllowedAndExponentialBackoff() throws Exception { RetryableException e = new RetryableException(null, null, null); Default retryer = new Retryer.Default(); assertEquals(retryer.attempt, 1); - assertEquals(retryer.sleptForNanos, 0); + assertEquals(retryer.sleptForMillis, 0); retryer.continueOrPropagate(e); assertEquals(retryer.attempt, 2); - assertEquals(retryer.sleptForNanos, 150000000); + assertEquals(retryer.sleptForMillis, 150); retryer.continueOrPropagate(e); assertEquals(retryer.attempt, 3); - assertEquals(retryer.sleptForNanos, 375000000); + assertEquals(retryer.sleptForMillis, 375); retryer.continueOrPropagate(e); assertEquals(retryer.attempt, 4); - assertEquals(retryer.sleptForNanos, 712500000); + assertEquals(retryer.sleptForMillis, 712); retryer.continueOrPropagate(e); assertEquals(retryer.attempt, 5); - assertEquals(retryer.sleptForNanos, 1218750000); + assertEquals(retryer.sleptForMillis, 1218); retryer.continueOrPropagate(e); // fail } @Test public void considersRetryAfterButNotMoreThanMaxPeriod() throws Exception { - Default retryer = new Retryer.Default(); - retryer.ticker = epoch; + Default retryer = new Retryer.Default() { + protected long currentTimeMillis() { + return 0; + } + }; retryer.continueOrPropagate(new RetryableException(null, null, new Date(5000))); assertEquals(retryer.attempt, 2); - assertEquals(retryer.sleptForNanos, 1000000000); + assertEquals(retryer.sleptForMillis, 1000); } - - static Ticker epoch = new Ticker() { - @Override - public long read() { - return 0; - } - }; - } diff --git a/feign-core/src/test/java/feign/FeignTest.java b/feign-core/src/test/java/feign/FeignTest.java index 7c8daaf437..c9fdf89dca 100644 --- a/feign-core/src/test/java/feign/FeignTest.java +++ b/feign-core/src/test/java/feign/FeignTest.java @@ -16,7 +16,6 @@ package feign; import com.google.common.collect.ImmutableMap; -import com.google.common.reflect.TypeToken; import com.google.mockwebserver.MockResponse; import com.google.mockwebserver.MockWebServer; import com.google.mockwebserver.SocketPolicy; @@ -25,6 +24,7 @@ import java.io.IOException; import java.io.Reader; +import java.lang.reflect.Type; import java.net.URI; import java.util.Map; @@ -73,7 +73,7 @@ public void canOverrideErrorDecoderOnMethod() throws IOException, InterruptedExc return ImmutableMap.of("TestInterface#post()", new ErrorDecoder() { @Override - public Object decode(String methodKey, Response response, TypeToken type) throws Throwable { + public Object decode(String methodKey, Response response, Type type) throws Throwable { if (response.status() == 404) throw new IllegalArgumentException("zone not found"); return ErrorDecoder.DEFAULT.decode(methodKey, response, type); @@ -126,7 +126,7 @@ public void doesntRetryAfterResponseIsSent() throws IOException, InterruptedExce return ImmutableMap.of("TestInterface", new Decoder() { @Override - public Object decode(String methodKey, Reader reader, TypeToken type) throws Throwable { + public Object decode(String methodKey, Reader reader, Type type) throws Throwable { throw new IOException("error reading response"); } diff --git a/feign-core/src/test/java/feign/RequestTemplateTest.java b/feign-core/src/test/java/feign/RequestTemplateTest.java index c28e35d45e..f13eeda7c9 100644 --- a/feign-core/src/test/java/feign/RequestTemplateTest.java +++ b/feign-core/src/test/java/feign/RequestTemplateTest.java @@ -68,13 +68,13 @@ public class RequestTemplateTest { .append("/?Action=DescribeRegions").query("RegionName.1", "{region}"); assertEquals(template.queries(), - ImmutableListMultimap.of("Action", "DescribeRegions", "RegionName.1", "{region}")); + ImmutableListMultimap.of("Action", "DescribeRegions", "RegionName.1", "{region}").asMap()); assertEquals(template.toString(), ""// + "GET /?Action=DescribeRegions&RegionName.1={region} HTTP/1.1\n"); template.resolve(ImmutableMap.of("region", "eu-west-1")); assertEquals(template.queries(), - ImmutableListMultimap.of("Action", "DescribeRegions", "RegionName.1", "eu-west-1")); + ImmutableListMultimap.of("Action", "DescribeRegions", "RegionName.1", "eu-west-1").asMap()); assertEquals(template.toString(), ""// + "GET /?Action=DescribeRegions&RegionName.1=eu-west-1 HTTP/1.1\n"); diff --git a/feign-core/src/test/java/feign/codec/DefaultErrorDecoderTest.java b/feign-core/src/test/java/feign/codec/DefaultErrorDecoderTest.java index 90734c52cf..835b50c7af 100644 --- a/feign-core/src/test/java/feign/codec/DefaultErrorDecoderTest.java +++ b/feign-core/src/test/java/feign/codec/DefaultErrorDecoderTest.java @@ -15,39 +15,41 @@ */ package feign.codec; -import com.google.common.collect.ImmutableListMultimap; -import com.google.common.reflect.TypeToken; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMultimap; import org.testng.annotations.Test; +import java.util.Collection; + import feign.FeignException; import feign.Response; import feign.RetryableException; -import static com.google.common.net.HttpHeaders.RETRY_AFTER; +import static feign.Util.RETRY_AFTER; public class DefaultErrorDecoderTest { @Test(expectedExceptions = FeignException.class, expectedExceptionsMessageRegExp = "status 500 reading Service#foo\\(\\)") public void throwsFeignException() throws Throwable { - Response response = Response.create(500, "Internal server error", ImmutableListMultimap.of(), + Response response = Response.create(500, "Internal server error", ImmutableMap.>of(), null); - ErrorDecoder.DEFAULT.decode("Service#foo()", response, TypeToken.of(Void.class)); + ErrorDecoder.DEFAULT.decode("Service#foo()", response, void.class); } @Test(expectedExceptions = FeignException.class, expectedExceptionsMessageRegExp = "status 500 reading Service#foo\\(\\); content:\nhello world") public void throwsFeignExceptionIncludingBody() throws Throwable { - Response response = Response.create(500, "Internal server error", ImmutableListMultimap.of(), + Response response = Response.create(500, "Internal server error", ImmutableMap.>of(), "hello world"); - ErrorDecoder.DEFAULT.decode("Service#foo()", response, TypeToken.of(Void.class)); + ErrorDecoder.DEFAULT.decode("Service#foo()", response, void.class); } @Test(expectedExceptions = RetryableException.class, expectedExceptionsMessageRegExp = "status 503 reading Service#foo\\(\\)") public void retryAfterHeaderThrowsRetryableException() throws Throwable { Response response = Response.create(503, "Service Unavailable", - ImmutableListMultimap.of(RETRY_AFTER, "Sat, 1 Jan 2000 00:00:00 GMT"), null); + ImmutableMultimap.of(RETRY_AFTER, "Sat, 1 Jan 2000 00:00:00 GMT").asMap(), null); - ErrorDecoder.DEFAULT.decode("Service#foo()", response, TypeToken.of(Void.class)); + ErrorDecoder.DEFAULT.decode("Service#foo()", response, void.class); } } diff --git a/feign-core/src/test/java/feign/codec/RetryAfterDecoderTest.java b/feign-core/src/test/java/feign/codec/RetryAfterDecoderTest.java index 1f4e473df5..7f4e4fbaca 100644 --- a/feign-core/src/test/java/feign/codec/RetryAfterDecoderTest.java +++ b/feign-core/src/test/java/feign/codec/RetryAfterDecoderTest.java @@ -15,8 +15,6 @@ */ package feign.codec; -import com.google.common.base.Ticker; - import org.testng.annotations.Test; import java.text.ParseException; @@ -31,31 +29,25 @@ public class RetryAfterDecoderTest { @Test public void malformDateFailsGracefully() { - assertFalse(decoder.apply("Fri, 31 Dec 1999 23:59:59 ZBW").isPresent()); + assertFalse(decoder.apply("Fri, 31 Dec 1999 23:59:59 ZBW") != null); } @Test public void rfc822Parses() throws ParseException { - assertEquals(decoder.apply("Fri, 31 Dec 1999 23:59:59 GMT").get(), + assertEquals(decoder.apply("Fri, 31 Dec 1999 23:59:59 GMT"), RFC822_FORMAT.parse("Fri, 31 Dec 1999 23:59:59 GMT")); } @Test public void relativeSecondsParses() throws ParseException { - assertEquals(decoder.apply("86400").get(), RFC822_FORMAT.parse("Sun, 2 Jan 2000 00:00:00 GMT")); + assertEquals(decoder.apply("86400"), RFC822_FORMAT.parse("Sun, 2 Jan 2000 00:00:00 GMT")); } - static Ticker y2k = new Ticker() { - - @Override - public long read() { + private RetryAfterDecoder decoder = new RetryAfterDecoder(RFC822_FORMAT) { + protected long currentTimeNanos() { try { return MILLISECONDS.toNanos(RFC822_FORMAT.parse("Sat, 1 Jan 2000 00:00:00 GMT").getTime()); } catch (ParseException e) { throw new RuntimeException(e); } } - }; - - private RetryAfterDecoder decoder = new RetryAfterDecoder(y2k, RFC822_FORMAT); - } diff --git a/feign-core/src/test/java/feign/examples/GitHubExample.java b/feign-core/src/test/java/feign/examples/GitHubExample.java index 26cfc74253..5da310df96 100644 --- a/feign-core/src/test/java/feign/examples/GitHubExample.java +++ b/feign-core/src/test/java/feign/examples/GitHubExample.java @@ -18,11 +18,11 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableMap; -import com.google.common.reflect.TypeToken; import com.google.gson.Gson; import java.io.IOException; import java.io.Reader; +import java.lang.reflect.Type; import java.util.List; import java.util.Map; @@ -77,8 +77,8 @@ static class GsonModule { final Decoder jsonDecoder = new Decoder() { Gson gson = new Gson(); - @Override public Object decode(String methodKey, Reader reader, TypeToken type) { - return gson.fromJson(reader, type.getType()); + @Override public Object decode(String methodKey, Reader reader, Type type) { + return gson.fromJson(reader, type); } }; } @@ -95,9 +95,9 @@ static class JacksonModule { final Decoder jsonDecoder = new Decoder() { ObjectMapper mapper = new ObjectMapper().disable(FAIL_ON_UNKNOWN_PROPERTIES).setVisibility(FIELD, ANY); - @Override public Object decode(String methodKey, Reader reader, final TypeToken type) + @Override public Object decode(String methodKey, Reader reader, final Type type) throws JsonProcessingException, IOException { - return mapper.readValue(reader, mapper.constructType(type.getType())); + return mapper.readValue(reader, mapper.constructType(type)); } }; } diff --git a/feign-core/src/test/java/feign/examples/IAMExample.java b/feign-core/src/test/java/feign/examples/IAMExample.java index 0d3b6eeb38..eacb1fc3f5 100644 --- a/feign-core/src/test/java/feign/examples/IAMExample.java +++ b/feign-core/src/test/java/feign/examples/IAMExample.java @@ -44,12 +44,12 @@ import feign.codec.Decoder; import feign.codec.Decoders; -import static com.google.common.base.Charsets.UTF_8; import static com.google.common.base.Throwables.propagate; import static com.google.common.collect.Iterables.transform; import static com.google.common.hash.Hashing.sha256; import static com.google.common.io.BaseEncoding.base16; -import static com.google.common.net.HttpHeaders.HOST; +import static feign.Util.HOST; +import static feign.Util.UTF_8; public class IAMExample { @@ -109,7 +109,7 @@ public AWSSignatureVersion4(String accessKey, String secretKey) { @Override public Request apply(RequestTemplate input) { input.header(HOST, URI.create(input.url()).getHost()); - Multimap sortedLowercaseHeaders = TreeMultimap.create(); + TreeMultimap sortedLowercaseHeaders = TreeMultimap.create(); for (String key : input.headers().keySet()) { sortedLowercaseHeaders.putAll(trimToLowercase.apply(key), transform(input.headers().get(key), trimToLowercase)); @@ -179,9 +179,9 @@ private String canonicalString(RequestTemplate input, Multimap s canonicalRequest.append(Joiner.on(',').join(sortedLowercaseHeaders.keySet())).append('\n'); // HexEncode(Hash(Payload)) - if (input.body().isPresent()) { + if (input.body() != null) { canonicalRequest.append(base16().lowerCase().encode( - sha256().hashString(input.body().or(""), UTF_8).asBytes())); + sha256().hashString(input.body() != null ? input.body() : "", UTF_8).asBytes())); } else { canonicalRequest.append(EMPTY_STRING_HASH); } diff --git a/feign-ribbon/src/main/java/feign/ribbon/LBClient.java b/feign-ribbon/src/main/java/feign/ribbon/LBClient.java index 923d20d179..134c289bf4 100644 --- a/feign-ribbon/src/main/java/feign/ribbon/LBClient.java +++ b/feign-ribbon/src/main/java/feign/ribbon/LBClient.java @@ -15,11 +15,6 @@ */ package feign.ribbon; -import com.google.common.collect.ImmutableListMultimap; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Iterables; -import com.google.common.collect.LinkedListMultimap; -import com.google.common.collect.ListMultimap; import com.netflix.client.AbstractLoadBalancerAwareClient; import com.netflix.client.ClientException; import com.netflix.client.ClientRequest; @@ -32,11 +27,7 @@ import java.io.IOException; import java.net.URI; import java.util.Collection; -import java.util.List; import java.util.Map; -import java.util.Set; - -import javax.ws.rs.core.MultivaluedMap; import feign.Client; import feign.Request; @@ -102,7 +93,7 @@ Request toRequest() { .method(request.method()) .append(getUri().toASCIIString()) .headers(request.headers()) - .body(request.body().orNull()).request(); + .body(request.body()).request(); } public Object clone() { @@ -121,11 +112,11 @@ static class RibbonResponse implements IResponse { } @Override public Object getPayload() throws ClientException { - return response.body().orNull(); + return response.body(); } @Override public boolean hasPayload() { - return response.body().isPresent(); + return response.body() != null; } @Override public boolean isSuccess() { @@ -137,7 +128,7 @@ static class RibbonResponse implements IResponse { } @Override public Map> getHeaders() { - return response.headers().asMap(); + return response.headers(); } Response toResponse() { diff --git a/feign-ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java b/feign-ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java index 337793ff2c..87287915e2 100644 --- a/feign-ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java +++ b/feign-ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java @@ -26,7 +26,7 @@ import feign.Target; import static com.google.common.base.Objects.equal; -import static com.google.common.base.Preconditions.checkNotNull; +import static feign.Util.checkNotNull; import static com.netflix.client.ClientFactory.getNamedLoadBalancer; import static java.lang.String.format; diff --git a/feign-ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java b/feign-ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java index aab95c05c2..1b041ae8d4 100644 --- a/feign-ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java +++ b/feign-ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java @@ -32,7 +32,7 @@ @Test public class LoadBalancingTargetTest { - static interface TestInterface { + interface TestInterface { @POST void post(); } diff --git a/feign-ribbon/src/test/java/feign/ribbon/RibbonClientTest.java b/feign-ribbon/src/test/java/feign/ribbon/RibbonClientTest.java index 4fdd6ff7c4..9f79a16174 100644 --- a/feign-ribbon/src/test/java/feign/ribbon/RibbonClientTest.java +++ b/feign-ribbon/src/test/java/feign/ribbon/RibbonClientTest.java @@ -32,7 +32,7 @@ @Test public class RibbonClientTest { - static interface TestInterface { + interface TestInterface { @POST void post(); } From 3726f089f6d411c13ecbf5b9c0679aeba1143444 Mon Sep 17 00:00:00 2001 From: adriancole Date: Sat, 29 Jun 2013 18:20:13 -0700 Subject: [PATCH 054/179] added new @RequestLine and @Headers annotations to make JAX-RS optional --- CHANGES.md | 4 + README.md | 14 +- build.gradle | 21 +- feign-core/src/main/java/feign/Body.java | 28 +++ feign-core/src/main/java/feign/Contract.java | 181 +++++++++------ feign-core/src/main/java/feign/Feign.java | 3 + feign-core/src/main/java/feign/Headers.java | 50 ++++ .../src/main/java/feign/ReflectiveFeign.java | 19 +- .../src/main/java/feign/RequestLine.java | 56 +++++ .../src/main/java/feign/RequestTemplate.java | 34 +-- .../main/java/feign/codec/FormEncoder.java | 2 +- .../test/java/feign/DefaultContractTest.java | 219 ++++++++++++++++++ feign-core/src/test/java/feign/FeignTest.java | 32 ++- .../test/java/feign/RequestTemplateTest.java | 55 ++++- .../feign/examples/AWSSignatureVersion4.java | 161 +++++++++++++ .../java/feign/examples/GitHubExample.java | 9 +- .../test/java/feign/examples/IAMExample.java | 144 +----------- .../main/java/feign/jaxrs/JAXRSModule.java | 106 +++++++++ .../java/feign/jaxrs/JAXRSContractTest.java | 101 ++++++-- .../feign/jaxrs/examples/GitHubExample.java | 79 +++++++ .../java/feign/jaxrs/examples/IAMExample.java | 79 +++++++ .../feign/ribbon/LoadBalancingTarget.java | 2 +- .../feign/ribbon/LoadBalancingTargetTest.java | 5 +- .../java/feign/ribbon/RibbonClientTest.java | 5 +- settings.gradle | 2 +- 25 files changed, 1111 insertions(+), 300 deletions(-) create mode 100644 feign-core/src/main/java/feign/Body.java create mode 100644 feign-core/src/main/java/feign/Headers.java create mode 100644 feign-core/src/main/java/feign/RequestLine.java create mode 100644 feign-core/src/test/java/feign/DefaultContractTest.java create mode 100644 feign-core/src/test/java/feign/examples/AWSSignatureVersion4.java create mode 100644 feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java rename feign-core/src/test/java/feign/ContractTest.java => feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java (53%) create mode 100644 feign-jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java create mode 100644 feign-jaxrs/src/test/java/feign/jaxrs/examples/IAMExample.java diff --git a/CHANGES.md b/CHANGES.md index 396970cd87..1f5d7de19d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,7 @@ +### Version 2.0.0 +* removes guava and jax-rs dependencies +* adds JAX-RS integration + ### Version 1.1.0 * adds Ribbon integration * adds cli example diff --git a/README.md b/README.md index bae3ef40f2..e63c3c0419 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ Usage typically looks like this, an adaptation of the [canonical Retrofit sample ```java interface GitHub { - @GET @Path("/repos/{owner}/{repo}/contributors") - List contributors(@PathParam("owner") String owner, @PathParam("repo") String repo); + @RequestLine("GET /repos/{owner}/{repo}/contributors") + List contributors(@Named("owner") String owner, @Named("repo") String repo); } static class Contributor { @@ -68,6 +68,16 @@ You can find [several examples](https://github.com/Netflix/feign/tree/master/fei ### Integrations Feign intends to work well within Netflix and other Open Source communities. Modules are welcome to integrate with your favorite projects! +### JAX-RS +[JAXRSModule](https://github.com/Netflix/feign/tree/master/feign-jaxrs) overrides annotation processing to instead use standard ones supplied by the JAX-RS specification. This is currently targeted at the 1.1 spec. + +Here's the example above re-written to use JAX-RS: +```java +interface GitHub { + @GET @Path("/repos/{owner}/{repo}/contributors") + List contributors(@PathParam("owner") String owner, @PathParam("repo") String repo); +} +``` ### Ribbon [RibbonModule](https://github.com/Netflix/feign/tree/master/feign-ribbon) overrides URL resolution of Feign's client, adding smart routing and resiliency capabilities provided by [Ribbon](https://github.com/Netflix/ribbon). diff --git a/build.gradle b/build.gradle index d12dd2cd47..56d1cd4312 100644 --- a/build.gradle +++ b/build.gradle @@ -36,7 +36,6 @@ project(':feign-core') { dependencies { compile 'com.squareup.dagger:dagger:1.0.1' - compile 'javax.ws.rs:jsr311-api:1.1.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' @@ -46,6 +45,26 @@ project(':feign-core') { } } +project(':feign-jaxrs') { + apply plugin: 'java' + + test { + useTestNG() + } + + 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 'com.google.guava:guava:14.0.1' + testCompile 'com.google.code.gson:gson:2.2.4' + testCompile 'org.testng:testng:6.8.1' + testCompile 'com.google.mockwebserver:mockwebserver:20130505' + } +} + project(':feign-ribbon') { apply plugin: 'java' diff --git a/feign-core/src/main/java/feign/Body.java b/feign-core/src/main/java/feign/Body.java new file mode 100644 index 0000000000..0104acfbf1 --- /dev/null +++ b/feign-core/src/main/java/feign/Body.java @@ -0,0 +1,28 @@ +package feign; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.Map; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * A possibly templated body of a PUT or POST command. variables wrapped in curly braces are expanded before the + * request is submitted. + *

+ * ex. + *

+ *

+ * @Body("<v01:getResourceRecordsOfZone><zoneName>{zoneName}</zoneName><rrType>0</rrType></v01:getResourceRecordsOfZone>")
+ * List<Record> listByZone(@Named("zoneName") String zoneName);
+ * 
+ *

+ * Note that if you'd like curly braces literally in the body, urlencode + * them first. + * + * @see RequestTemplate#expand(String, Map) + */ +@Target(METHOD) @Retention(RUNTIME) public @interface Body { + String value(); +} diff --git a/feign-core/src/main/java/feign/Contract.java b/feign-core/src/main/java/feign/Contract.java index 031fa302df..407bedce34 100644 --- a/feign-core/src/main/java/feign/Contract.java +++ b/feign-core/src/main/java/feign/Contract.java @@ -20,30 +20,23 @@ import java.net.URI; import java.util.ArrayList; import java.util.Collection; -import java.util.LinkedHashSet; -import java.util.Set; - -import javax.ws.rs.Consumes; -import javax.ws.rs.FormParam; -import javax.ws.rs.HeaderParam; -import javax.ws.rs.HttpMethod; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; - -import static feign.Util.ACCEPT; -import static feign.Util.CONTENT_TYPE; +import java.util.List; + +import javax.inject.Named; + import static feign.Util.checkState; -import static feign.Util.join; +import static feign.Util.emptyToNull; /** * Defines what annotations and values are valid on interfaces. */ -public final class Contract { +public abstract class Contract { - public static Set parseAndValidatateMetadata(Class declaring) { - Set metadata = new LinkedHashSet(); + /** + * Called to parse the methods in the class that are linked to HTTP requests. + */ + public List parseAndValidatateMetadata(Class declaring) { + List metadata = new ArrayList(); for (Method method : declaring.getDeclaredMethods()) { if (method.getDeclaringClass() == Object.class) continue; @@ -52,75 +45,31 @@ public static Set parseAndValidatateMetadata(Class declaring) return metadata; } - public static MethodMetadata parseAndValidatateMetadata(Method method) { + /** + * Called indirectly by {@link #parseAndValidatateMetadata(Class)}. + */ + public MethodMetadata parseAndValidatateMetadata(Method method) { MethodMetadata data = new MethodMetadata(); data.returnType(method.getGenericReturnType()); data.configKey(Feign.configKey(method)); for (Annotation methodAnnotation : method.getAnnotations()) { - Class annotationType = methodAnnotation.annotationType(); - HttpMethod http = annotationType.getAnnotation(HttpMethod.class); - if (http != null) { - checkState(data.template().method() == null, - "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 == RequestTemplate.Body.class) { - String body = RequestTemplate.Body.class.cast(methodAnnotation).value(); - if (body.indexOf('{') == -1) { - data.template().body(body); - } else { - data.template().bodyTemplate(body); - } - } else if (annotationType == Path.class) { - data.template().append(Path.class.cast(methodAnnotation).value()); - } else if (annotationType == Produces.class) { - data.template().header(CONTENT_TYPE, join(',', ((Produces) methodAnnotation).value())); - } else if (annotationType == Consumes.class) { - data.template().header(ACCEPT, join(',', ((Consumes) methodAnnotation).value())); - } + processAnnotationOnMethod(data, methodAnnotation, method); } checkState(data.template().method() != null, "Method %s not annotated with HTTP method type (ex. GET, POST)", method.getName()); Class[] parameterTypes = method.getParameterTypes(); - Annotation[][] parameterAnnotationArrays = method.getParameterAnnotations(); - int count = parameterAnnotationArrays.length; + Annotation[][] parameterAnnotations = method.getParameterAnnotations(); + int count = parameterAnnotations.length; for (int i = 0; i < count; i++) { - boolean hasHttpAnnotation = false; - - Class parameterType = parameterTypes[i]; - Annotation[] parameterAnnotations = parameterAnnotationArrays[i]; - if (parameterAnnotations != null) { - for (Annotation parameterAnnotation : parameterAnnotations) { - Class annotationType = parameterAnnotation.annotationType(); - if (annotationType == PathParam.class) { - indexName(data, i, PathParam.class.cast(parameterAnnotation).value()); - hasHttpAnnotation = true; - } else if (annotationType == QueryParam.class) { - String name = QueryParam.class.cast(parameterAnnotation).value(); - Collection query = addTemplatedParam(data.template().queries().get(name), name); - data.template().query(name, query); - indexName(data, i, name); - hasHttpAnnotation = true; - } else if (annotationType == HeaderParam.class) { - String name = HeaderParam.class.cast(parameterAnnotation).value(); - Collection header = addTemplatedParam(data.template().headers().get(name), name); - data.template().header(name, header); - indexName(data, i, name); - hasHttpAnnotation = true; - } else if (annotationType == FormParam.class) { - String form = FormParam.class.cast(parameterAnnotation).value(); - data.formParams().add(form); - indexName(data, i, form); - hasHttpAnnotation = true; - } - } + boolean isHttpAnnotation = false; + if (parameterAnnotations[i] != null) { + isHttpAnnotation = processAnnotationsOnParameter(data, parameterAnnotations[i], i); } - - if (parameterType == URI.class) { + if (parameterTypes[i] == URI.class) { data.urlIndex(i); - } else if (!hasHttpAnnotation) { + } else if (!isHttpAnnotation) { checkState(data.formParams().isEmpty(), "Body parameters cannot be used with @FormParam parameters."); checkState(data.bodyIndex() == null, "Method has too many Body parameters: %s", method); data.bodyIndex(i); @@ -129,16 +78,96 @@ public static MethodMetadata parseAndValidatateMetadata(Method method) { return data; } - private static Collection addTemplatedParam(Collection possiblyNull, String name) { + /** + * @param data metadata collected so far relating to the current java method. + * @param annotation annotations present on the current method annotation. + * @param method method currently being processed. + */ + protected abstract void processAnnotationOnMethod(MethodMetadata data, Annotation annotation, Method method); + + /** + * @param data metadata collected so far relating to the current java method. + * @param annotations annotations present on the current parameter annotation. + * @param paramIndex if you find a name in {@code annotations}, call {@link #nameParam(MethodMetadata, String, + * int)} with this as the last parameter. + * @return true if you called {@link #nameParam(MethodMetadata, String, int)} after finding an http-relevant + * annotation. + */ + protected abstract boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex); + + + protected Collection addTemplatedParam(Collection possiblyNull, String name) { if (possiblyNull == null) possiblyNull = new ArrayList(); possiblyNull.add(String.format("{%s}", name)); return possiblyNull; } - private static void indexName(MethodMetadata data, int i, String name) { + /** + * links a parameter name to its index in the method signature. + */ + protected void nameParam(MethodMetadata data, String name, int i) { Collection names = data.indexToName().containsKey(i) ? data.indexToName().get(i) : new ArrayList(); names.add(name); data.indexToName().put(i, names); } + + static class DefaultContract extends Contract { + + @Override + protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) { + Class annotationType = methodAnnotation.annotationType(); + if (annotationType == RequestLine.class) { + String requestLine = RequestLine.class.cast(methodAnnotation).value(); + checkState(emptyToNull(requestLine) != null, "RequestLine annotation was empty on method %s.", method.getName()); + if (requestLine.indexOf(' ') == -1) { + data.template().method(requestLine); + return; + } + data.template().method(requestLine.substring(0, requestLine.indexOf(' '))); + if (requestLine.indexOf(' ') == requestLine.lastIndexOf(' ')) { + // no HTTP version is ok + data.template().append(requestLine.substring(requestLine.indexOf(' ') + 1)); + } else { + // skip HTTP version + data.template().append(requestLine.substring(requestLine.indexOf(' ') + 1, requestLine.lastIndexOf(' '))); + } + } else if (annotationType == Body.class) { + String body = Body.class.cast(methodAnnotation).value(); + checkState(emptyToNull(body) != null, "Body annotation was empty on method %s.", method.getName()); + if (body.indexOf('{') == -1) { + data.template().body(body); + } else { + data.template().bodyTemplate(body); + } + } else if (annotationType == Headers.class) { + String[] headersToParse = Headers.class.cast(methodAnnotation).value(); + checkState(headersToParse.length > 0, "Headers annotation was empty on method %s.", method.getName()); + for (String header : headersToParse) { + int colon = header.indexOf(':'); + data.template().header(header.substring(0, colon), header.substring(colon + 2)); + } + } + } + + @Override + protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) { + boolean isHttpAnnotation = false; + for (Annotation parameterAnnotation : annotations) { + Class annotationType = parameterAnnotation.annotationType(); + if (annotationType == Named.class) { + String name = Named.class.cast(parameterAnnotation).value(); + checkState(emptyToNull(name) != null, "Named annotation was empty on param %s.", paramIndex); + nameParam(data, name, paramIndex); + isHttpAnnotation = true; + if (data.template().url().indexOf('{' + name + '}') == -1 && // + !(data.template().queries().containsKey(name) + || data.template().headers().containsKey(name))) { + data.formParams().add(name); + } + } + } + return isHttpAnnotation; + } + } } diff --git a/feign-core/src/main/java/feign/Feign.java b/feign-core/src/main/java/feign/Feign.java index fe27c83b57..ea8b8b3628 100644 --- a/feign-core/src/main/java/feign/Feign.java +++ b/feign-core/src/main/java/feign/Feign.java @@ -80,6 +80,9 @@ public static ObjectGraph createObjectGraph(Object... modules) { @dagger.Module(complete = false, injects = Feign.class, library = true) public static class Defaults { + @Provides Contract contract() { + return new Contract.DefaultContract(); + } @Provides SSLSocketFactory sslSocketFactory() { return SSLSocketFactory.class.cast(SSLSocketFactory.getDefault()); diff --git a/feign-core/src/main/java/feign/Headers.java b/feign-core/src/main/java/feign/Headers.java new file mode 100644 index 0000000000..ad96930b06 --- /dev/null +++ b/feign-core/src/main/java/feign/Headers.java @@ -0,0 +1,50 @@ +package feign; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Expands headers supplied in the {@code value}. Variables are permitted as values. + *

+ *

+ * @RequestLine("GET /")
+ * @Headers("Cache-Control: max-age=640000")
+ * ...
+ *
+ * @RequestLine("POST /")
+ * @Headers({
+ *   "X-Foo: Bar",
+ *   "X-Ping: {token}"
+ * }) void post(@Named("token") String token);
+ * ...
+ * 
+ *

+ * Note: Headers do not overwrite each other. All headers with the same name will + * be included in the request. + *

Relationship to JAXRS

+ *

+ * The following two forms are identical. + *

+ * Feign: + *

+ * @RequestLine("POST /")
+ * @Headers({
+ *   "X-Ping: {token}"
+ * }) void post(@Named("token") String token);
+ * ...
+ * 
+ *

+ * JAX-RS: + *

+ * @POST @Path("/")
+ * void post(@HeaderParam("X-Ping") String token);
+ * ...
+ * 
+ */ +@Target(METHOD) @Retention(RUNTIME) +public @interface Headers { + String[] value(); +} diff --git a/feign-core/src/main/java/feign/ReflectiveFeign.java b/feign-core/src/main/java/feign/ReflectiveFeign.java index 2bd0beea77..c9c623932a 100644 --- a/feign-core/src/main/java/feign/ReflectiveFeign.java +++ b/feign-core/src/main/java/feign/ReflectiveFeign.java @@ -20,9 +20,9 @@ import java.lang.reflect.Proxy; import java.util.Collection; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.Set; import javax.inject.Inject; @@ -35,7 +35,6 @@ import feign.codec.FormEncoder; import feign.codec.ToStringDecoder; -import static feign.Contract.parseAndValidatateMetadata; import static feign.Util.checkArgument; import static feign.Util.checkNotNull; import static java.lang.String.format; @@ -113,6 +112,7 @@ private static IllegalStateException noConfig(String configKey, Class type) { } static final class ParseHandlersByName { + private final Contract contract; private final Map options; private final Map bodyEncoders; private final Map formEncoders; @@ -120,9 +120,10 @@ static final class ParseHandlersByName { private final Map errorDecoders; private final Factory factory; - @Inject ParseHandlersByName(Map options, Map bodyEncoders, + @Inject ParseHandlersByName(Contract contract, Map options, Map bodyEncoders, Map formEncoders, Map decoders, Map errorDecoders, Factory factory) { + this.contract = contract; this.options = options; this.bodyEncoders = bodyEncoders; this.formEncoders = formEncoders; @@ -132,7 +133,7 @@ static final class ParseHandlersByName { } public Map apply(Target key) { - Set metadata = parseAndValidatateMetadata(key.type()); + List metadata = contract.parseAndValidatateMetadata(key.type()); Map result = new LinkedHashMap(); for (MethodMetadata md : metadata) { Options options = forMethodOrClass(this.options, md.configKey()); @@ -151,24 +152,24 @@ public Map apply(Target key) { if (errorDecoder == null) { errorDecoder = ErrorDecoder.DEFAULT; } - BuildTemplateByResolvingArgs BuildTemplateByResolvingArgs; + BuildTemplateByResolvingArgs buildTemplate; if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) { FormEncoder formEncoder = forMethodOrClass(formEncoders, md.configKey()); if (formEncoder == null) { throw noConfig(md.configKey(), FormEncoder.class); } - BuildTemplateByResolvingArgs = new BuildFormEncodedTemplateFromArgs(md, formEncoder); + buildTemplate = new BuildFormEncodedTemplateFromArgs(md, formEncoder); } else if (md.bodyIndex() != null) { BodyEncoder bodyEncoder = forMethodOrClass(bodyEncoders, md.configKey()); if (bodyEncoder == null) { throw noConfig(md.configKey(), BodyEncoder.class); } - BuildTemplateByResolvingArgs = new BuildBodyEncodedTemplateFromArgs(md, bodyEncoder); + buildTemplate = new BuildBodyEncodedTemplateFromArgs(md, bodyEncoder); } else { - BuildTemplateByResolvingArgs = new BuildTemplateByResolvingArgs(md); + buildTemplate = new BuildTemplateByResolvingArgs(md); } result.put(md.configKey(), - factory.create(key, md, BuildTemplateByResolvingArgs, options, decoder, errorDecoder)); + factory.create(key, md, buildTemplate, options, decoder, errorDecoder)); } return result; } diff --git a/feign-core/src/main/java/feign/RequestLine.java b/feign-core/src/main/java/feign/RequestLine.java new file mode 100644 index 0000000000..9d19862c60 --- /dev/null +++ b/feign-core/src/main/java/feign/RequestLine.java @@ -0,0 +1,56 @@ +package feign; + +import java.lang.annotation.Retention; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Expands the request-line supplied in the {@code value}, permitting path and query variables, + * or just the http method. + *

+ *

+ * ...
+ * @RequestLine("POST /servers")
+ * ...
+ *
+ * @RequestLine("GET /servers/{serverId}?count={count}")
+ * void get(@Named("serverId") String serverId, @Named("count") int count);
+ * ...
+ *
+ * @RequestLine("GET")
+ * Response getNext(URI nextLink);
+ * ...
+ * 
+ * HTTP version suffix is optional, but permitted. There are no guarantees this version will impact that + * sent by the client. + *

+ *

+ * @RequestLine("POST /servers HTTP/1.1")
+ * ...
+ * 
+ *

+ * Note: Query params do not overwrite each other. All queries with the same name will + * be included in the request. + *

Relationship to JAXRS

+ *

+ * The following two forms are identical. + *

+ * Feign: + *

+ * @RequestLine("GET /servers/{serverId}?count={count}")
+ * void get(@Named("serverId") String serverId, @Named("count") int count);
+ * ...
+ * 
+ *

+ * JAX-RS: + *

+ * @GET @Path("/servers/{serverId}")
+ * void get(@PathParam("serverId") String serverId, @QueryParam("count") int count);
+ * ...
+ * 
+ */ +@java.lang.annotation.Target(METHOD) @Retention(RUNTIME) +public @interface RequestLine { + String value(); +} diff --git a/feign-core/src/main/java/feign/RequestTemplate.java b/feign-core/src/main/java/feign/RequestTemplate.java index 9dfe51e238..5f085bee3b 100644 --- a/feign-core/src/main/java/feign/RequestTemplate.java +++ b/feign-core/src/main/java/feign/RequestTemplate.java @@ -17,8 +17,6 @@ import java.io.Serializable; import java.io.UnsupportedEncodingException; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; import java.net.URLDecoder; import java.net.URLEncoder; import java.util.ArrayList; @@ -26,6 +24,7 @@ import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -35,8 +34,6 @@ import static feign.Util.emptyToNull; import static feign.Util.toArray; import static feign.Util.valuesOrEmpty; -import static java.lang.annotation.ElementType.METHOD; -import static java.lang.annotation.RetentionPolicy.RUNTIME; /** * Builds a request to an http target. Not thread safe. @@ -50,26 +47,6 @@ */ public final class RequestTemplate implements Serializable { - /** - * A templatized form for a PUT or POST command. Values of {@link javax.ws.rs.PathParam}, - * {@link javax.ws.rs.QueryParam}, {@link javax.ws.rs.HeaderParam}, and {@link javax.ws.rs.FormParam} can be - * used are passed to the template. - *

- * ex. - *

- *

-   * @Body("<v01:getResourceRecordsOfZone><zoneName>{zoneName}</zoneName><rrType>0</rrType></v01:getResourceRecordsOfZone>")
-   * List<Record> listByZone(@PayloadParam("zoneName") String zoneName);
-   * 
- *

- * Note that if you'd like curly braces literally in the body, urlencode - * them first. - * - * @see RequestTemplate#expand(String, Map) - */ - @Target(METHOD) @Retention(RUNTIME) public @interface Body { - String value(); - } private String method; /* final to encourage mutable use vs replacing the object. */ @@ -368,10 +345,13 @@ public Map> queries() { */ public RequestTemplate header(String configKey, String... values) { checkNotNull(configKey, "header configKey"); - if (values == null || (values.length == 1 && values[0] == null)) + if (values == null || (values.length == 1 && values[0] == null)) { headers.remove(configKey); - else - this.headers.put(configKey, Arrays.asList(values)); + } else { + List headers = new ArrayList(); + headers.addAll(Arrays.asList(values)); + this.headers.put(configKey, headers); + } return this; } diff --git a/feign-core/src/main/java/feign/codec/FormEncoder.java b/feign-core/src/main/java/feign/codec/FormEncoder.java index 08e77a43bb..3d3ab1e828 100644 --- a/feign-core/src/main/java/feign/codec/FormEncoder.java +++ b/feign-core/src/main/java/feign/codec/FormEncoder.java @@ -24,7 +24,7 @@ public interface FormEncoder { /** * FormParam encoding *

- * If any parameters are annotated with {@link javax.ws.rs.FormParam}, they will be + * If any parameters are found in {@link feign.MethodMetadata#formParams()}, they will be * collected and passed as {code formParams} *

*

diff --git a/feign-core/src/test/java/feign/DefaultContractTest.java b/feign-core/src/test/java/feign/DefaultContractTest.java
new file mode 100644
index 0000000000..3642bb0604
--- /dev/null
+++ b/feign-core/src/test/java/feign/DefaultContractTest.java
@@ -0,0 +1,219 @@
+/*
+ * 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.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
+import org.testng.annotations.Test;
+
+import java.lang.annotation.*;
+import java.net.URI;
+
+import javax.inject.Named;
+
+import static feign.Util.CONTENT_TYPE;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.*;
+import static org.testng.Assert.assertTrue;
+
+/**
+ * Tests interfaces defined per {@link feign.Contract.DefaultContract} are interpreted into expected {@link feign
+ * .RequestTemplate template}
+ * instances.
+ */
+@Test
+public class DefaultContractTest {
+  Contract.DefaultContract contract = new Contract.DefaultContract();
+
+  interface Methods {
+    @RequestLine("POST /") void post();
+
+    @RequestLine("PUT /") void put();
+
+    @RequestLine("GET /") void get();
+
+    @RequestLine("DELETE /") void delete();
+  }
+
+  @Test public void httpMethods() throws Exception {
+    assertEquals(contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("post")).template().method(),
+        "POST");
+    assertEquals(contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("put")).template().method(),
+        "PUT");
+    assertEquals(contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("get")).template().method(),
+        "GET");
+    assertEquals(contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("delete")).template().method(),
+        "DELETE");
+  }
+
+  interface CustomMethodAndURIParam {
+    @RequestLine("PATCH") Response patch(URI nextLink);
+  }
+
+  @Test public void requestLineOnlyRequiresMethod() throws Exception {
+    MethodMetadata md = contract.parseAndValidatateMetadata(CustomMethodAndURIParam.class.getDeclaredMethod("patch",
+        URI.class));
+    assertEquals(md.template().method(), "PATCH");
+    assertEquals(md.template().url(), "");
+    assertTrue(md.template().queries().isEmpty());
+    assertTrue(md.template().headers().isEmpty());
+    assertNull(md.template().body());
+    assertNull(md.template().bodyTemplate());
+    assertEquals(md.urlIndex(), Integer.valueOf(0));
+  }
+
+  interface WithQueryParamsInPath {
+    @RequestLine("GET /") Response none();
+
+    @RequestLine("GET /?Action=GetUser") Response one();
+
+    @RequestLine("GET /?Action=GetUser&Version=2010-05-08") Response two();
+
+    @RequestLine("GET /?Action=GetUser&Version=2010-05-08&limit=1") Response three();
+
+    @RequestLine("GET /?flag&Action=GetUser&Version=2010-05-08") Response empty();
+  }
+
+  @Test public void queryParamsInPathExtract() throws Exception {
+    {
+      MethodMetadata md = contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("none"));
+      assertEquals(md.template().url(), "/");
+      assertTrue(md.template().queries().isEmpty());
+      assertEquals(md.template().toString(), "GET / HTTP/1.1\n");
+    }
+    {
+      MethodMetadata md = contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("one"));
+      assertEquals(md.template().url(), "/");
+      assertEquals(md.template().queries().get("Action"), ImmutableSet.of("GetUser"));
+      assertEquals(md.template().toString(), "GET /?Action=GetUser HTTP/1.1\n");
+    }
+    {
+      MethodMetadata md = contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("two"));
+      assertEquals(md.template().url(), "/");
+      assertEquals(md.template().queries().get("Action"), ImmutableSet.of("GetUser"));
+      assertEquals(md.template().queries().get("Version"), ImmutableSet.of("2010-05-08"));
+      assertEquals(md.template().toString(), "GET /?Action=GetUser&Version=2010-05-08 HTTP/1.1\n");
+    }
+    {
+      MethodMetadata md = contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("three"));
+      assertEquals(md.template().url(), "/");
+      assertEquals(md.template().queries().get("Action"), ImmutableSet.of("GetUser"));
+      assertEquals(md.template().queries().get("Version"), ImmutableSet.of("2010-05-08"));
+      assertEquals(md.template().queries().get("limit"), ImmutableSet.of("1"));
+      assertEquals(md.template().toString(), "GET /?Action=GetUser&Version=2010-05-08&limit=1 HTTP/1.1\n");
+    }
+    {
+      MethodMetadata md = contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("empty"));
+      assertEquals(md.template().url(), "/");
+      assertTrue(md.template().queries().containsKey("flag"));
+      assertEquals(md.template().queries().get("Action"), ImmutableSet.of("GetUser"));
+      assertEquals(md.template().queries().get("Version"), ImmutableSet.of("2010-05-08"));
+      assertEquals(md.template().toString(), "GET /?flag&Action=GetUser&Version=2010-05-08 HTTP/1.1\n");
+    }
+  }
+
+  interface BodyWithoutParameters {
+    @RequestLine("POST /")
+    @Headers("Content-Type: application/xml")
+    @Body("") Response post();
+  }
+
+  @Test public void bodyWithoutParameters() throws Exception {
+    MethodMetadata md = contract.parseAndValidatateMetadata(BodyWithoutParameters.class.getDeclaredMethod("post"));
+    assertEquals(md.template().body(), "");
+    assertFalse(md.template().bodyTemplate() != null);
+    assertTrue(md.formParams().isEmpty());
+    assertTrue(md.indexToName().isEmpty());
+  }
+
+  @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"));
+  }
+
+  interface WithURIParam {
+    @RequestLine("GET /{1}/{2}") Response uriParam(@Named("1") String one, URI endpoint, @Named("2") String two);
+  }
+
+  @Test public void methodCanHaveUriParam() throws Exception {
+    MethodMetadata md = contract.parseAndValidatateMetadata(WithURIParam.class.getDeclaredMethod("uriParam", String.class,
+        URI.class, String.class));
+    assertEquals(md.urlIndex(), Integer.valueOf(1));
+  }
+
+  @Test public void pathParamsParseIntoIndexToName() throws Exception {
+    MethodMetadata md = contract.parseAndValidatateMetadata(WithURIParam.class.getDeclaredMethod("uriParam", String.class,
+        URI.class, String.class));
+    assertEquals(md.template().url(), "/{1}/{2}");
+    assertEquals(md.indexToName().get(0), ImmutableSet.of("1"));
+    assertEquals(md.indexToName().get(2), ImmutableSet.of("2"));
+  }
+
+  interface WithPathAndQueryParams {
+    @RequestLine("GET /domains/{domainId}/records?name={name}&type={type}")
+    Response recordsByNameAndType(@Named("domainId") int id, @Named("name") String nameFilter,
+                                  @Named("type") String typeFilter);
+  }
+
+  @Test public void mixedRequestLineParams() throws Exception {
+    MethodMetadata md = contract.parseAndValidatateMetadata(WithPathAndQueryParams.class.getDeclaredMethod
+        ("recordsByNameAndType", int.class, String.class, String.class));
+    assertNull(md.template().body());
+    assertNull(md.template().bodyTemplate());
+    assertTrue(md.template().headers().isEmpty());
+    assertEquals(md.template().url(), "/domains/{domainId}/records");
+    assertEquals(md.template().queries().get("name"), ImmutableSet.of("{name}"));
+    assertEquals(md.template().queries().get("type"), ImmutableSet.of("{type}"));
+    assertEquals(md.indexToName().get(0), ImmutableSet.of("domainId"));
+    assertEquals(md.indexToName().get(1), ImmutableSet.of("name"));
+    assertEquals(md.indexToName().get(2), ImmutableSet.of("type"));
+    assertEquals(md.template().toString(), "GET /domains/{domainId}/records?name={name}&type={type} HTTP/1.1\n");
+  }
+
+  interface FormParams {
+    @RequestLine("POST /")
+    @Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D")
+    void login(
+        @Named("customer_name") String customer,
+        @Named("user_name") String user, @Named("password") String password);
+  }
+
+  @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");
+    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"));
+  }
+
+  interface HeaderParams {
+    @RequestLine("POST /")
+    @Headers("Auth-Token: {Auth-Token}") void logout(@Named("Auth-Token") String token);
+  }
+
+  @Test public void headerParamsParseIntoIndexToName() throws Exception {
+    MethodMetadata md = contract.parseAndValidatateMetadata(HeaderParams.class.getDeclaredMethod("logout", String.class));
+
+    assertEquals(md.template().headers().get("Auth-Token"), ImmutableSet.of("{Auth-Token}"));
+    assertEquals(md.indexToName().get(0), ImmutableSet.of("Auth-Token"));
+  }
+}
diff --git a/feign-core/src/test/java/feign/FeignTest.java b/feign-core/src/test/java/feign/FeignTest.java
index c9fdf89dca..306de900fc 100644
--- a/feign-core/src/test/java/feign/FeignTest.java
+++ b/feign-core/src/test/java/feign/FeignTest.java
@@ -28,12 +28,9 @@
 import java.net.URI;
 import java.util.Map;
 
+import javax.inject.Named;
 import javax.inject.Singleton;
 import javax.net.ssl.SSLSocketFactory;
-import javax.ws.rs.GET;
-import javax.ws.rs.POST;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
 
 import dagger.Module;
 import dagger.Provides;
@@ -46,9 +43,15 @@
 @Test
 public class FeignTest {
   interface TestInterface {
-    @POST String post();
+    @RequestLine("POST /") String post();
 
-    @GET @Path("/{1}/{2}") Response uriParam(@PathParam("1") String one, URI endpoint, @PathParam("2") String two);
+    @RequestLine("POST /")
+    @Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D")
+    void login(
+        @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);
 
     @dagger.Module(overrides = true, library = true)
     static class Module {
@@ -60,6 +63,23 @@ static class Module {
     }
   }
 
+  @Test
+  public void postTemplateParamsResolve() throws IOException, InterruptedException {
+    final MockWebServer server = new MockWebServer();
+    server.enqueue(new MockResponse().setResponseCode(200).setBody("foo"));
+    server.play();
+
+    try {
+      TestInterface api = Feign.create(TestInterface.class, server.getUrl("").toString(), new TestInterface.Module());
+
+      api.login("netflix", "denominator", "password");
+      assertEquals(new String(server.takeRequest().getBody()),
+          "{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}");
+    } finally {
+      server.shutdown();
+    }
+  }
+
   @Test public void toKeyMethodFormatsAsExpected() throws Exception {
     assertEquals(Feign.configKey(TestInterface.class.getDeclaredMethod("post")), "TestInterface#post()");
     assertEquals(Feign.configKey(TestInterface.class.getDeclaredMethod("uriParam", String.class, URI.class,
diff --git a/feign-core/src/test/java/feign/RequestTemplateTest.java b/feign-core/src/test/java/feign/RequestTemplateTest.java
index f13eeda7c9..173ce535e7 100644
--- a/feign-core/src/test/java/feign/RequestTemplateTest.java
+++ b/feign-core/src/test/java/feign/RequestTemplateTest.java
@@ -22,7 +22,6 @@
 import org.testng.annotations.Test;
 
 import static feign.RequestTemplate.expand;
-import static javax.ws.rs.HttpMethod.GET;
 import static org.testng.Assert.assertEquals;
 
 public class RequestTemplateTest {
@@ -46,7 +45,7 @@ public class RequestTemplateTest {
 
   @Test public void resolveTemplateWithParameterizedPathSkipsEncodingSlash() {
 
-    RequestTemplate template = new RequestTemplate().method(GET)
+    RequestTemplate template = new RequestTemplate().method("GET")
         .append("{zoneId}");
 
     assertEquals(template.toString(), ""//
@@ -64,7 +63,7 @@ public class RequestTemplateTest {
   }
 
   @Test public void resolveTemplateWithBaseAndParameterizedQuery() {
-    RequestTemplate template = new RequestTemplate().method(GET)
+    RequestTemplate template = new RequestTemplate().method("GET")
         .append("/?Action=DescribeRegions").query("RegionName.1", "{region}");
 
     assertEquals(template.queries(),
@@ -84,4 +83,54 @@ public class RequestTemplateTest {
     assertEquals(template.request().toString(), ""//
         + "GET https://iam.amazonaws.com/?Action=DescribeRegions&RegionName.1=eu-west-1 HTTP/1.1\n");
   }
+
+  @Test public void resolveTemplateWithMixedRequestLineParams() throws Exception {
+    RequestTemplate template = new RequestTemplate().method("GET")//
+        .append("/domains/{domainId}/records")//
+        .query("name", "{name}")//
+        .query("type", "{type}");
+
+    template = template.resolve(ImmutableMap.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://dns.api.rackspacecloud.com/v1.0/1234");
+
+    assertEquals(template.request().toString(), ""//
+        + "GET https://dns.api.rackspacecloud.com/v1.0/1234"//
+        + "/domains/1001/records?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}\", " +
+            "\"password\": \"{password}\"%7D");
+
+    template = template.resolve(ImmutableMap.builder()//
+        .put("customer_name", "netflix")//
+        .put("user_name", "denominator")//
+        .put("password", "password")//
+        .build()
+    );
+
+    assertEquals(template.toString(), ""//
+        + "POST  HTTP/1.1\n"//
+        + "Content-Length: 80\n"//
+        + "\n"//
+        + "{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}");
+
+    template.insert(0, "https://api2.dynect.net/REST");
+
+    assertEquals(template.request().toString(), ""//
+        + "POST https://api2.dynect.net/REST HTTP/1.1\n" //
+        + "Content-Length: 80\n" //
+        + "\n" //
+        + "{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}");
+  }
 }
diff --git a/feign-core/src/test/java/feign/examples/AWSSignatureVersion4.java b/feign-core/src/test/java/feign/examples/AWSSignatureVersion4.java
new file mode 100644
index 0000000000..dedc5a63b7
--- /dev/null
+++ b/feign-core/src/test/java/feign/examples/AWSSignatureVersion4.java
@@ -0,0 +1,161 @@
+/*
+ * 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.examples;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.TreeMultimap;
+
+import java.net.URI;
+import java.text.SimpleDateFormat;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Map.Entry;
+import java.util.TimeZone;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+import feign.Request;
+import feign.RequestTemplate;
+
+import static com.google.common.base.Throwables.propagate;
+import static com.google.common.collect.Iterables.transform;
+import static com.google.common.hash.Hashing.sha256;
+import static com.google.common.io.BaseEncoding.base16;
+import static feign.Util.HOST;
+import static feign.Util.UTF_8;
+
+// http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
+public class AWSSignatureVersion4 implements Function {
+
+  String region = "us-east-1";
+  String service = "iam";
+  String accessKey;
+  String secretKey;
+
+  public AWSSignatureVersion4(String accessKey, String secretKey) {
+    this.accessKey = accessKey;
+    this.secretKey = secretKey;
+  }
+
+  @Override public Request apply(RequestTemplate input) {
+    input.header(HOST, URI.create(input.url()).getHost());
+    TreeMultimap sortedLowercaseHeaders = TreeMultimap.create();
+    for (String key : input.headers().keySet()) {
+      sortedLowercaseHeaders.putAll(trimToLowercase.apply(key),
+          transform(input.headers().get(key), trimToLowercase));
+    }
+
+    String timestamp = iso8601.format(new Date());
+    String credentialScope = Joiner.on('/').join(timestamp.substring(0, 8), region, service, "aws4_request");
+
+    input.query("X-Amz-Algorithm", "AWS4-HMAC-SHA256");
+    input.query("X-Amz-Credential", accessKey + "/" + credentialScope);
+    input.query("X-Amz-Date", timestamp);
+    input.query("X-Amz-SignedHeaders", Joiner.on(';').join(sortedLowercaseHeaders.keySet()));
+
+    String canonicalString = canonicalString(input, sortedLowercaseHeaders);
+    String toSign = toSign(timestamp, credentialScope, canonicalString);
+
+    byte[] signatureKey = signatureKey(secretKey, timestamp);
+    String signature = base16().lowerCase().encode(hmacSHA256(toSign, signatureKey));
+
+    input.query("X-Amz-Signature", signature);
+
+    return input.request();
+  }
+
+  byte[] signatureKey(String secretKey, String timestamp) {
+    byte[] kSecret = ("AWS4" + secretKey).getBytes(UTF_8);
+    byte[] kDate = hmacSHA256(timestamp.substring(0, 8), kSecret);
+    byte[] kRegion = hmacSHA256(region, kDate);
+    byte[] kService = hmacSHA256(service, kRegion);
+    byte[] kSigning = hmacSHA256("aws4_request", kService);
+    return kSigning;
+  }
+
+  static byte[] hmacSHA256(String data, byte[] key) {
+    try {
+      String algorithm = "HmacSHA256";
+      Mac mac = Mac.getInstance(algorithm);
+      mac.init(new SecretKeySpec(key, algorithm));
+      return mac.doFinal(data.getBytes(UTF_8));
+    } catch (Exception e) {
+      throw propagate(e);
+    }
+  }
+
+  private static final String EMPTY_STRING_HASH = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
+
+  private String canonicalString(RequestTemplate input, Multimap sortedLowercaseHeaders) {
+    StringBuilder canonicalRequest = new StringBuilder();
+    // HTTPRequestMethod + '\n' +
+    canonicalRequest.append(input.method()).append('\n');
+
+    // CanonicalURI + '\n' +
+    canonicalRequest.append(URI.create(input.url()).getPath()).append('\n');
+
+    // CanonicalQueryString + '\n' +
+    canonicalRequest.append(input.queryLine().substring(1));
+    canonicalRequest.append('\n');
+
+    // CanonicalHeaders + '\n' +
+    for (Entry> entry : sortedLowercaseHeaders.asMap().entrySet()) {
+      canonicalRequest.append(entry.getKey()).append(':').append(Joiner.on(',').join(entry.getValue()))
+          .append('\n');
+    }
+    canonicalRequest.append('\n');
+
+    // SignedHeaders + '\n' +
+    canonicalRequest.append(Joiner.on(',').join(sortedLowercaseHeaders.keySet())).append('\n');
+
+    // HexEncode(Hash(Payload))
+    if (input.body() != null) {
+      canonicalRequest.append(base16().lowerCase().encode(
+          sha256().hashString(input.body() != null ? input.body() : "", UTF_8).asBytes()));
+    } else {
+      canonicalRequest.append(EMPTY_STRING_HASH);
+    }
+    return canonicalRequest.toString();
+  }
+
+  private static final Function trimToLowercase = new Function() {
+    public String apply(String in) {
+      return in.toLowerCase().trim();
+    }
+  };
+
+  private String toSign(String timestamp, String credentialScope, String canonicalRequest) {
+    StringBuilder toSign = new StringBuilder();
+    // Algorithm + '\n' +
+    toSign.append("AWS4-HMAC-SHA256").append('\n');
+    // RequestDate + '\n' +
+    toSign.append(timestamp).append('\n');
+    // CredentialScope + '\n' +
+    toSign.append(credentialScope).append('\n');
+    // HexEncode(Hash(CanonicalRequest))
+    toSign.append(base16().lowerCase().encode(sha256().hashString(canonicalRequest, UTF_8).asBytes()));
+    return toSign.toString();
+  }
+
+  private static final SimpleDateFormat iso8601 = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
+
+  static {
+    iso8601.setTimeZone(TimeZone.getTimeZone("GMT"));
+  }
+}
diff --git a/feign-core/src/test/java/feign/examples/GitHubExample.java b/feign-core/src/test/java/feign/examples/GitHubExample.java
index 5da310df96..93d7108048 100644
--- a/feign-core/src/test/java/feign/examples/GitHubExample.java
+++ b/feign-core/src/test/java/feign/examples/GitHubExample.java
@@ -26,14 +26,13 @@
 import java.util.List;
 import java.util.Map;
 
+import javax.inject.Named;
 import javax.inject.Singleton;
-import javax.ws.rs.GET;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
 
 import dagger.Module;
 import dagger.Provides;
 import feign.Feign;
+import feign.RequestLine;
 import feign.codec.Decoder;
 
 import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY;
@@ -46,8 +45,8 @@
 public class GitHubExample {
 
   interface GitHub {
-    @GET @Path("/repos/{owner}/{repo}/contributors") List contributors(
-        @PathParam("owner") String owner, @PathParam("repo") String repo);
+    @RequestLine("GET /repos/{owner}/{repo}/contributors")
+    List contributors(@Named("owner") String owner, @Named("repo") String repo);
   }
 
   static class Contributor {
diff --git a/feign-core/src/test/java/feign/examples/IAMExample.java b/feign-core/src/test/java/feign/examples/IAMExample.java
index eacb1fc3f5..fccafbfeef 100644
--- a/feign-core/src/test/java/feign/examples/IAMExample.java
+++ b/feign-core/src/test/java/feign/examples/IAMExample.java
@@ -15,46 +15,26 @@
  */
 package feign.examples;
 
-import com.google.common.base.Function;
-import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Multimap;
-import com.google.common.collect.TreeMultimap;
 
-import java.net.URI;
-import java.text.SimpleDateFormat;
-import java.util.Collection;
-import java.util.Date;
 import java.util.Map;
-import java.util.Map.Entry;
-import java.util.TimeZone;
 
-import javax.crypto.Mac;
-import javax.crypto.spec.SecretKeySpec;
 import javax.inject.Singleton;
-import javax.ws.rs.GET;
-import javax.ws.rs.Path;
 
 import dagger.Module;
 import dagger.Provides;
 import feign.Feign;
 import feign.Request;
+import feign.RequestLine;
 import feign.RequestTemplate;
 import feign.Target;
 import feign.codec.Decoder;
 import feign.codec.Decoders;
 
-import static com.google.common.base.Throwables.propagate;
-import static com.google.common.collect.Iterables.transform;
-import static com.google.common.hash.Hashing.sha256;
-import static com.google.common.io.BaseEncoding.base16;
-import static feign.Util.HOST;
-import static feign.Util.UTF_8;
-
 public class IAMExample {
 
   interface IAM {
-    @GET @Path("/?Action=GetUser&Version=2010-05-08") String arn();
+    @RequestLine("GET /?Action=GetUser&Version=2010-05-08") String arn();
   }
 
   public static void main(String... args) {
@@ -93,124 +73,4 @@ static class IAMModule {
       return ImmutableMap.of("IAM#arn()", Decoders.firstGroup("([\\S&&[^<]]+)"));
     }
   }
-
-  // http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
-  static class AWSSignatureVersion4 implements Function {
-
-    String region = "us-east-1";
-    String service = "iam";
-    String accessKey;
-    String secretKey;
-
-    public AWSSignatureVersion4(String accessKey, String secretKey) {
-      this.accessKey = accessKey;
-      this.secretKey = secretKey;
-    }
-
-    @Override public Request apply(RequestTemplate input) {
-      input.header(HOST, URI.create(input.url()).getHost());
-      TreeMultimap sortedLowercaseHeaders = TreeMultimap.create();
-      for (String key : input.headers().keySet()) {
-        sortedLowercaseHeaders.putAll(trimToLowercase.apply(key),
-            transform(input.headers().get(key), trimToLowercase));
-      }
-
-      String timestamp = iso8601.format(new Date());
-      String credentialScope = Joiner.on('/').join(timestamp.substring(0, 8), region, service, "aws4_request");
-
-      input.query("X-Amz-Algorithm", "AWS4-HMAC-SHA256");
-      input.query("X-Amz-Credential", accessKey + "/" + credentialScope);
-      input.query("X-Amz-Date", timestamp);
-      input.query("X-Amz-SignedHeaders", Joiner.on(';').join(sortedLowercaseHeaders.keySet()));
-
-      String canonicalString = canonicalString(input, sortedLowercaseHeaders);
-      String toSign = toSign(timestamp, credentialScope, canonicalString);
-
-      byte[] signatureKey = signatureKey(secretKey, timestamp);
-      String signature = base16().lowerCase().encode(hmacSHA256(toSign, signatureKey));
-
-      input.query("X-Amz-Signature", signature);
-
-      return input.request();
-    }
-
-    byte[] signatureKey(String secretKey, String timestamp) {
-      byte[] kSecret = ("AWS4" + secretKey).getBytes(UTF_8);
-      byte[] kDate = hmacSHA256(timestamp.substring(0, 8), kSecret);
-      byte[] kRegion = hmacSHA256(region, kDate);
-      byte[] kService = hmacSHA256(service, kRegion);
-      byte[] kSigning = hmacSHA256("aws4_request", kService);
-      return kSigning;
-    }
-
-    static byte[] hmacSHA256(String data, byte[] key) {
-      try {
-        String algorithm = "HmacSHA256";
-        Mac mac = Mac.getInstance(algorithm);
-        mac.init(new SecretKeySpec(key, algorithm));
-        return mac.doFinal(data.getBytes(UTF_8));
-      } catch (Exception e) {
-        throw propagate(e);
-      }
-    }
-
-    private static final String EMPTY_STRING_HASH = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
-
-    private String canonicalString(RequestTemplate input, Multimap sortedLowercaseHeaders) {
-      StringBuilder canonicalRequest = new StringBuilder();
-      // HTTPRequestMethod + '\n' +
-      canonicalRequest.append(input.method()).append('\n');
-
-      // CanonicalURI + '\n' +
-      canonicalRequest.append(URI.create(input.url()).getPath()).append('\n');
-
-      // CanonicalQueryString + '\n' +
-      canonicalRequest.append(input.queryLine().substring(1));
-      canonicalRequest.append('\n');
-
-      // CanonicalHeaders + '\n' +
-      for (Entry> entry : sortedLowercaseHeaders.asMap().entrySet()) {
-        canonicalRequest.append(entry.getKey()).append(':').append(Joiner.on(',').join(entry.getValue()))
-            .append('\n');
-      }
-      canonicalRequest.append('\n');
-
-      // SignedHeaders + '\n' +
-      canonicalRequest.append(Joiner.on(',').join(sortedLowercaseHeaders.keySet())).append('\n');
-
-      // HexEncode(Hash(Payload))
-      if (input.body() != null) {
-        canonicalRequest.append(base16().lowerCase().encode(
-            sha256().hashString(input.body() != null ? input.body() : "", UTF_8).asBytes()));
-      } else {
-        canonicalRequest.append(EMPTY_STRING_HASH);
-      }
-      return canonicalRequest.toString();
-    }
-
-    private static final Function trimToLowercase = new Function() {
-      public String apply(String in) {
-        return in.toLowerCase().trim();
-      }
-    };
-
-    private String toSign(String timestamp, String credentialScope, String canonicalRequest) {
-      StringBuilder toSign = new StringBuilder();
-      // Algorithm + '\n' +
-      toSign.append("AWS4-HMAC-SHA256").append('\n');
-      // RequestDate + '\n' +
-      toSign.append(timestamp).append('\n');
-      // CredentialScope + '\n' +
-      toSign.append(credentialScope).append('\n');
-      // HexEncode(Hash(CanonicalRequest))
-      toSign.append(base16().lowerCase().encode(sha256().hashString(canonicalRequest, UTF_8).asBytes()));
-      return toSign.toString();
-    }
-
-    private static final SimpleDateFormat iso8601 = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
-
-    static {
-      iso8601.setTimeZone(TimeZone.getTimeZone("GMT"));
-    }
-  }
 }
diff --git a/feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java b/feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java
new file mode 100644
index 0000000000..07d6b9399d
--- /dev/null
+++ b/feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java
@@ -0,0 +1,106 @@
+/*
+ * 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;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.Collection;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.FormParam;
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+
+import dagger.Provides;
+import feign.Body;
+import feign.Contract;
+import feign.MethodMetadata;
+
+import static feign.Util.ACCEPT;
+import static feign.Util.CONTENT_TYPE;
+import static feign.Util.checkState;
+import static feign.Util.join;
+
+@dagger.Module(library = true)
+public final class JAXRSModule {
+
+  @Provides Contract provideContract() {
+    return new JAXRSContract();
+  }
+
+  static final class JAXRSContract extends Contract {
+
+    @Override
+    protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) {
+      Class annotationType = methodAnnotation.annotationType();
+      HttpMethod http = annotationType.getAnnotation(HttpMethod.class);
+      if (http != null) {
+        checkState(data.template().method() == null,
+            "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) {
+        data.template().append(Path.class.cast(methodAnnotation).value());
+      } else if (annotationType == Produces.class) {
+        data.template().header(CONTENT_TYPE, join(',', ((Produces) methodAnnotation).value()));
+      } else if (annotationType == Consumes.class) {
+        data.template().header(ACCEPT, join(',', ((Consumes) methodAnnotation).value()));
+      }
+    }
+
+    @Override
+    protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) {
+      boolean isHttpParam = false;
+      for (Annotation parameterAnnotation : annotations) {
+        Class annotationType = parameterAnnotation.annotationType();
+        if (annotationType == PathParam.class) {
+          String name = PathParam.class.cast(parameterAnnotation).value();
+          nameParam(data, name, paramIndex);
+          isHttpParam = true;
+        } else if (annotationType == QueryParam.class) {
+          String name = QueryParam.class.cast(parameterAnnotation).value();
+          Collection 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();
+          Collection 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();
+          data.formParams().add(name);
+          nameParam(data, name, paramIndex);
+          isHttpParam = true;
+        }
+      }
+      return isHttpParam;
+    }
+  }
+}
diff --git a/feign-core/src/test/java/feign/ContractTest.java b/feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java
similarity index 53%
rename from feign-core/src/test/java/feign/ContractTest.java
rename to feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java
index 164d7b78c3..8aaa24ed36 100644
--- a/feign-core/src/test/java/feign/ContractTest.java
+++ b/feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java
@@ -13,28 +13,37 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package feign;
+package feign.jaxrs;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 
 import org.testng.annotations.Test;
 
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
 import java.net.URI;
 
+import javax.inject.Named;
 import javax.ws.rs.DELETE;
 import javax.ws.rs.FormParam;
 import javax.ws.rs.GET;
 import javax.ws.rs.HeaderParam;
+import javax.ws.rs.HttpMethod;
 import javax.ws.rs.POST;
 import javax.ws.rs.PUT;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
 
-import feign.RequestTemplate.Body;
+import feign.Body;
+import feign.MethodMetadata;
+import feign.RequestLine;
+import feign.Response;
 
-import static feign.Contract.parseAndValidatateMetadata;
 import static feign.Util.CONTENT_TYPE;
 import static javax.ws.rs.HttpMethod.DELETE;
 import static javax.ws.rs.HttpMethod.GET;
@@ -43,14 +52,17 @@
 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;
 
 /**
- * Tests interfaces defined per {@link Contract} are interpreted into expected {@link RequestTemplate template}
+ * Tests interfaces defined per {@link feign.jaxrs.JAXRSModule.JAXRSContract} are interpreted into expected {@link feign
+ * .RequestTemplate template}
  * instances.
  */
 @Test
-public class ContractTest {
+public class JAXRSContractTest {
+  JAXRSModule.JAXRSContract contract = new JAXRSModule.JAXRSContract();
 
   interface Methods {
     @POST void post();
@@ -63,10 +75,33 @@ interface Methods {
   }
 
   @Test public void httpMethods() throws Exception {
-    assertEquals(parseAndValidatateMetadata(Methods.class.getDeclaredMethod("post")).template().method(), POST);
-    assertEquals(parseAndValidatateMetadata(Methods.class.getDeclaredMethod("put")).template().method(), PUT);
-    assertEquals(parseAndValidatateMetadata(Methods.class.getDeclaredMethod("get")).template().method(), GET);
-    assertEquals(parseAndValidatateMetadata(Methods.class.getDeclaredMethod("delete")).template().method(), DELETE);
+    assertEquals(contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("post")).template().method(),
+        POST);
+    assertEquals(contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("put")).template().method(), PUT);
+    assertEquals(contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("get")).template().method(), GET);
+    assertEquals(contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("delete")).template().method(), DELETE);
+  }
+
+  interface CustomMethodAndURIParam {
+    @Target({ElementType.METHOD})
+    @Retention(RetentionPolicy.RUNTIME)
+    @HttpMethod("PATCH")
+    public @interface PATCH {
+    }
+
+    @PATCH Response patch(URI nextLink);
+  }
+
+  @Test public void requestLineOnlyRequiresMethod() throws Exception {
+    MethodMetadata md = contract.parseAndValidatateMetadata(CustomMethodAndURIParam.class.getDeclaredMethod("patch",
+        URI.class));
+    assertEquals(md.template().method(), "PATCH");
+    assertEquals(md.template().url(), "");
+    assertTrue(md.template().queries().isEmpty());
+    assertTrue(md.template().headers().isEmpty());
+    assertNull(md.template().body());
+    assertNull(md.template().bodyTemplate());
+    assertEquals(md.urlIndex(), Integer.valueOf(0));
   }
 
   interface WithQueryParamsInPath {
@@ -83,34 +118,39 @@ interface WithQueryParamsInPath {
 
   @Test public void queryParamsInPathExtract() throws Exception {
     {
-      MethodMetadata md = parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("none"));
+      MethodMetadata md = contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("none"));
       assertEquals(md.template().url(), "/");
       assertTrue(md.template().queries().isEmpty());
+      assertEquals(md.template().toString(), "GET / HTTP/1.1\n");
     }
     {
-      MethodMetadata md = parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("one"));
+      MethodMetadata md = contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("one"));
       assertEquals(md.template().url(), "/");
       assertEquals(md.template().queries().get("Action"), ImmutableSet.of("GetUser"));
+      assertEquals(md.template().toString(), "GET /?Action=GetUser HTTP/1.1\n");
     }
     {
-      MethodMetadata md = parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("two"));
+      MethodMetadata md = contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("two"));
       assertEquals(md.template().url(), "/");
       assertEquals(md.template().queries().get("Action"), ImmutableSet.of("GetUser"));
       assertEquals(md.template().queries().get("Version"), ImmutableSet.of("2010-05-08"));
+      assertEquals(md.template().toString(), "GET /?Action=GetUser&Version=2010-05-08 HTTP/1.1\n");
     }
     {
-      MethodMetadata md = parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("three"));
+      MethodMetadata md = contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("three"));
       assertEquals(md.template().url(), "/");
       assertEquals(md.template().queries().get("Action"), ImmutableSet.of("GetUser"));
       assertEquals(md.template().queries().get("Version"), ImmutableSet.of("2010-05-08"));
       assertEquals(md.template().queries().get("limit"), ImmutableSet.of("1"));
+      assertEquals(md.template().toString(), "GET /?Action=GetUser&Version=2010-05-08&limit=1 HTTP/1.1\n");
     }
     {
-      MethodMetadata md = parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("empty"));
+      MethodMetadata md = contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("empty"));
       assertEquals(md.template().url(), "/");
       assertTrue(md.template().queries().containsKey("flag"));
       assertEquals(md.template().queries().get("Action"), ImmutableSet.of("GetUser"));
       assertEquals(md.template().queries().get("Version"), ImmutableSet.of("2010-05-08"));
+      assertEquals(md.template().toString(), "GET /?flag&Action=GetUser&Version=2010-05-08 HTTP/1.1\n");
     }
   }
 
@@ -119,7 +159,7 @@ interface BodyWithoutParameters {
   }
 
   @Test public void bodyWithoutParameters() throws Exception {
-    MethodMetadata md = parseAndValidatateMetadata(BodyWithoutParameters.class.getDeclaredMethod("post"));
+    MethodMetadata md = contract.parseAndValidatateMetadata(BodyWithoutParameters.class.getDeclaredMethod("post"));
     assertEquals(md.template().body(), "");
     assertFalse(md.template().bodyTemplate() != null);
     assertTrue(md.formParams().isEmpty());
@@ -127,7 +167,7 @@ interface BodyWithoutParameters {
   }
 
   @Test public void producesAddsContentTypeHeader() throws Exception {
-    MethodMetadata md = parseAndValidatateMetadata(BodyWithoutParameters.class.getDeclaredMethod("post"));
+    MethodMetadata md = contract.parseAndValidatateMetadata(BodyWithoutParameters.class.getDeclaredMethod("post"));
     assertEquals(md.template().headers().get(CONTENT_TYPE), ImmutableSet.of(APPLICATION_XML));
   }
 
@@ -136,19 +176,40 @@ interface WithURIParam {
   }
 
   @Test public void methodCanHaveUriParam() throws Exception {
-    MethodMetadata md = parseAndValidatateMetadata(WithURIParam.class.getDeclaredMethod("uriParam", String.class,
+    MethodMetadata md = contract.parseAndValidatateMetadata(WithURIParam.class.getDeclaredMethod("uriParam", String.class,
         URI.class, String.class));
     assertEquals(md.urlIndex(), Integer.valueOf(1));
   }
 
   @Test public void pathParamsParseIntoIndexToName() throws Exception {
-    MethodMetadata md = parseAndValidatateMetadata(WithURIParam.class.getDeclaredMethod("uriParam", String.class,
+    MethodMetadata md = contract.parseAndValidatateMetadata(WithURIParam.class.getDeclaredMethod("uriParam", String.class,
         URI.class, String.class));
     assertEquals(md.template().url(), "/{1}/{2}");
     assertEquals(md.indexToName().get(0), ImmutableSet.of("1"));
     assertEquals(md.indexToName().get(2), ImmutableSet.of("2"));
   }
 
+  interface WithPathAndQueryParams {
+    @GET @Path("/domains/{domainId}/records")
+    Response recordsByNameAndType(@PathParam("domainId") int id, @QueryParam("name") String nameFilter,
+                                  @QueryParam("type") String typeFilter);
+  }
+
+  @Test public void mixedRequestLineParams() throws Exception {
+    MethodMetadata md = contract.parseAndValidatateMetadata(WithPathAndQueryParams.class.getDeclaredMethod
+        ("recordsByNameAndType", int.class, String.class, String.class));
+    assertNull(md.template().body());
+    assertNull(md.template().bodyTemplate());
+    assertTrue(md.template().headers().isEmpty());
+    assertEquals(md.template().url(), "/domains/{domainId}/records");
+    assertEquals(md.template().queries().get("name"), ImmutableSet.of("{name}"));
+    assertEquals(md.template().queries().get("type"), ImmutableSet.of("{type}"));
+    assertEquals(md.indexToName().get(0), ImmutableSet.of("domainId"));
+    assertEquals(md.indexToName().get(1), ImmutableSet.of("name"));
+    assertEquals(md.indexToName().get(2), ImmutableSet.of("type"));
+    assertEquals(md.template().toString(), "GET /domains/{domainId}/records?name={name}&type={type} HTTP/1.1\n");
+  }
+
   interface FormParams {
     @POST
     @Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D")
@@ -158,7 +219,7 @@ void login(
   }
 
   @Test public void formParamsParseIntoIndexToName() throws Exception {
-    MethodMetadata md = parseAndValidatateMetadata(FormParams.class.getDeclaredMethod("login", String.class,
+    MethodMetadata md = contract.parseAndValidatateMetadata(FormParams.class.getDeclaredMethod("login", String.class,
         String.class, String.class));
 
     assertFalse(md.template().body() != null);
@@ -175,7 +236,7 @@ interface HeaderParams {
   }
 
   @Test public void headerParamsParseIntoIndexToName() throws Exception {
-    MethodMetadata md = parseAndValidatateMetadata(HeaderParams.class.getDeclaredMethod("logout", String.class));
+    MethodMetadata md = contract.parseAndValidatateMetadata(HeaderParams.class.getDeclaredMethod("logout", String.class));
 
     assertEquals(md.template().headers().get("Auth-Token"), ImmutableSet.of("{Auth-Token}"));
     assertEquals(md.indexToName().get(0), ImmutableSet.of("Auth-Token"));
diff --git a/feign-jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java b/feign-jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java
new file mode 100644
index 0000000000..3499a85151
--- /dev/null
+++ b/feign-jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java
@@ -0,0 +1,79 @@
+/*
+ * 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 com.google.common.collect.ImmutableMap;
+import com.google.gson.Gson;
+
+import java.io.Reader;
+import java.lang.reflect.Type;
+import java.util.List;
+import java.util.Map;
+
+import javax.inject.Singleton;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+
+import dagger.Module;
+import dagger.Provides;
+import feign.Feign;
+import feign.codec.Decoder;
+import feign.jaxrs.JAXRSModule;
+
+/**
+ * adapted from {@code com.example.retrofit.GitHubClient}
+ */
+public class GitHubExample {
+
+  interface GitHub {
+    @GET @Path("/repos/{owner}/{repo}/contributors") List contributors(
+        @PathParam("owner") String owner, @PathParam("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 GitHubModule());
+
+    // Fetch and print a list of the contributors to this library.
+    List contributors = github.contributors("netflix", "feign");
+    for (Contributor contributor : contributors) {
+      System.out.println(contributor.login + " (" + contributor.contributions + ")");
+    }
+  }
+
+  /**
+   * JAXRSModule tells us to process @GET etc annotations
+   */
+  @Module(overrides = true, library = true, includes = JAXRSModule.class)
+  static class GitHubModule {
+    @Provides @Singleton Map decoders() {
+      return ImmutableMap.of("GitHub", jsonDecoder);
+    }
+
+    final Decoder jsonDecoder = new Decoder() {
+      Gson gson = new Gson();
+
+      @Override public Object decode(String methodKey, Reader reader, Type type) {
+        return gson.fromJson(reader, type);
+      }
+    };
+  }
+}
diff --git a/feign-jaxrs/src/test/java/feign/jaxrs/examples/IAMExample.java b/feign-jaxrs/src/test/java/feign/jaxrs/examples/IAMExample.java
new file mode 100644
index 0000000000..c46c6420db
--- /dev/null
+++ b/feign-jaxrs/src/test/java/feign/jaxrs/examples/IAMExample.java
@@ -0,0 +1,79 @@
+/*
+ * 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 com.google.common.collect.ImmutableMap;
+
+import java.util.Map;
+
+import javax.inject.Singleton;
+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;
+
+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 {
+
+    @Override public Class 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 @Singleton Map decoders() {
+      return ImmutableMap.of("IAM#arn()", Decoders.firstGroup("([\\S&&[^<]]+)"));
+    }
+  }
+}
diff --git a/feign-ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java b/feign-ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java
index 87287915e2..e7bcadc42e 100644
--- a/feign-ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java
+++ b/feign-ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java
@@ -26,8 +26,8 @@
 import feign.Target;
 
 import static com.google.common.base.Objects.equal;
-import static feign.Util.checkNotNull;
 import static com.netflix.client.ClientFactory.getNamedLoadBalancer;
+import static feign.Util.checkNotNull;
 import static java.lang.String.format;
 
 /**
diff --git a/feign-ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java b/feign-ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java
index 1b041ae8d4..29e834e0d5 100644
--- a/feign-ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java
+++ b/feign-ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java
@@ -23,9 +23,8 @@
 import java.io.IOException;
 import java.net.URL;
 
-import javax.ws.rs.POST;
-
 import feign.Feign;
+import feign.RequestLine;
 
 import static com.netflix.config.ConfigurationManager.getConfigInstance;
 import static org.testng.Assert.assertEquals;
@@ -33,7 +32,7 @@
 @Test
 public class LoadBalancingTargetTest {
   interface TestInterface {
-    @POST void post();
+    @RequestLine("POST /") void post();
   }
 
   @Test
diff --git a/feign-ribbon/src/test/java/feign/ribbon/RibbonClientTest.java b/feign-ribbon/src/test/java/feign/ribbon/RibbonClientTest.java
index 9f79a16174..2f49d56959 100644
--- a/feign-ribbon/src/test/java/feign/ribbon/RibbonClientTest.java
+++ b/feign-ribbon/src/test/java/feign/ribbon/RibbonClientTest.java
@@ -23,9 +23,8 @@
 import java.io.IOException;
 import java.net.URL;
 
-import javax.ws.rs.POST;
-
 import feign.Feign;
+import feign.RequestLine;
 
 import static com.netflix.config.ConfigurationManager.getConfigInstance;
 import static org.testng.Assert.assertEquals;
@@ -33,7 +32,7 @@
 @Test
 public class RibbonClientTest {
   interface TestInterface {
-    @POST void post();
+    @RequestLine("POST /") void post();
   }
 
   @Test
diff --git a/settings.gradle b/settings.gradle
index d2dc7844e3..dc5b04fffb 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,2 +1,2 @@
 rootProject.name='feign'
-include 'feign-core', 'feign-ribbon', 'examples:feign-example-cli'
+include 'feign-core', 'feign-jaxrs', 'feign-ribbon', 'examples:feign-example-cli'

From 664123157913df482eb1fd40af23d129328aaa37 Mon Sep 17 00:00:00 2001
From: adriancole 
Date: Mon, 1 Jul 2013 09:30:23 -0700
Subject: [PATCH 055/179] comply with jdk 8 javadoc strictness

---
 feign-core/src/main/java/feign/Body.java      |  6 +-
 feign-core/src/main/java/feign/Client.java    |  2 -
 feign-core/src/main/java/feign/Feign.java     |  8 +--
 feign-core/src/main/java/feign/Headers.java   | 12 ++--
 feign-core/src/main/java/feign/Request.java   |  6 +-
 .../src/main/java/feign/RequestLine.java      | 14 ++--
 .../src/main/java/feign/RequestTemplate.java  | 66 +++++++++----------
 feign-core/src/main/java/feign/Response.java  |  8 +--
 feign-core/src/main/java/feign/Retryer.java   |  2 +-
 feign-core/src/main/java/feign/Target.java    | 16 ++---
 .../main/java/feign/codec/BodyEncoder.java    |  8 +--
 .../src/main/java/feign/codec/Decoder.java    | 10 +--
 .../src/main/java/feign/codec/Decoders.java   | 24 +++----
 .../main/java/feign/codec/ErrorDecoder.java   | 15 ++---
 .../main/java/feign/codec/FormEncoder.java    |  4 +-
 .../test/java/feign/DefaultContractTest.java  |  4 +-
 .../java/feign/jaxrs/JAXRSContractTest.java   |  2 -
 .../feign/ribbon/LoadBalancingTarget.java     |  2 +-
 .../main/java/feign/ribbon/RibbonModule.java  |  4 +-
 19 files changed, 104 insertions(+), 109 deletions(-)

diff --git a/feign-core/src/main/java/feign/Body.java b/feign-core/src/main/java/feign/Body.java
index 0104acfbf1..f4d5d2bdc9 100644
--- a/feign-core/src/main/java/feign/Body.java
+++ b/feign-core/src/main/java/feign/Body.java
@@ -10,14 +10,14 @@
 /**
  * A possibly templated body of a PUT or POST command. variables wrapped in curly braces are expanded before the
  * request is submitted.
- * 

+ *
* ex. - *

+ *
*

  * @Body("<v01:getResourceRecordsOfZone><zoneName>{zoneName}</zoneName><rrType>0</rrType></v01:getResourceRecordsOfZone>")
  * List<Record> listByZone(@Named("zoneName") String zoneName);
  * 
- *

+ *
* Note that if you'd like curly braces literally in the body, urlencode * them first. * diff --git a/feign-core/src/main/java/feign/Client.java b/feign-core/src/main/java/feign/Client.java index 8a7b08cf3c..2b7a1afefe 100644 --- a/feign-core/src/main/java/feign/Client.java +++ b/feign-core/src/main/java/feign/Client.java @@ -19,9 +19,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; -import java.io.OutputStreamWriter; import java.io.Reader; -import java.io.Writer; import java.net.HttpURLConnection; import java.net.URL; import java.util.Collection; diff --git a/feign-core/src/main/java/feign/Feign.java b/feign-core/src/main/java/feign/Feign.java index ea8b8b3628..7cfa35864e 100644 --- a/feign-core/src/main/java/feign/Feign.java +++ b/feign-core/src/main/java/feign/Feign.java @@ -36,7 +36,7 @@ /** * Feign's purpose is to ease development against http apis that feign * restfulness. - *

+ *
* In implementation, Feign is a {@link Feign#newInstance factory} for * generating {@link Target targeted} http apis. */ @@ -122,11 +122,11 @@ public static class Defaults { } /** - *

+ *
* Configuration keys are formatted as unresolved see tags. - *

+ *
* For example. *

    *
  • {@code Route53}: would match a class such as @@ -138,7 +138,7 @@ public static class Defaults { *
  • {@code Route53#listByNameAndType(String, String)}: would match a * method such as {@code denominator.route53.Route53#listAt(String, String)} *
- *

+ *
* Note that there is no whitespace expected in a key! */ public static String configKey(Method method) { diff --git a/feign-core/src/main/java/feign/Headers.java b/feign-core/src/main/java/feign/Headers.java index ad96930b06..b1d7061fe1 100644 --- a/feign-core/src/main/java/feign/Headers.java +++ b/feign-core/src/main/java/feign/Headers.java @@ -8,7 +8,7 @@ /** * Expands headers supplied in the {@code value}. Variables are permitted as values. - *

+ *
*

  * @RequestLine("GET /")
  * @Headers("Cache-Control: max-age=640000")
@@ -21,13 +21,13 @@
  * }) void post(@Named("token") String token);
  * ...
  * 
- *

+ *
* Note: Headers do not overwrite each other. All headers with the same name will * be included in the request. - *

Relationship to JAXRS

- *

+ *

Relationship to JAXRS
+ *
* The following two forms are identical. - *

+ *
* Feign: *

  * @RequestLine("POST /")
@@ -36,7 +36,7 @@
  * }) void post(@Named("token") String token);
  * ...
  * 
- *

+ *
* JAX-RS: *

  * @POST @Path("/")
diff --git a/feign-core/src/main/java/feign/Request.java b/feign-core/src/main/java/feign/Request.java
index 3df1613c45..b40c956a05 100644
--- a/feign-core/src/main/java/feign/Request.java
+++ b/feign-core/src/main/java/feign/Request.java
@@ -25,9 +25,9 @@
 
 /**
  * An immutable request to an http server.
- * 

- *

Note

- *

+ *
+ *

Note
+ *
* Since {@link Feign} is designed for non-binary apis, and expectations are * that any request can be replayed, we only support a String body. */ diff --git a/feign-core/src/main/java/feign/RequestLine.java b/feign-core/src/main/java/feign/RequestLine.java index 9d19862c60..b344144c53 100644 --- a/feign-core/src/main/java/feign/RequestLine.java +++ b/feign-core/src/main/java/feign/RequestLine.java @@ -8,7 +8,7 @@ /** * Expands the request-line supplied in the {@code value}, permitting path and query variables, * or just the http method. - *

+ *
*

  * ...
  * @RequestLine("POST /servers")
@@ -24,25 +24,25 @@
  * 
* HTTP version suffix is optional, but permitted. There are no guarantees this version will impact that * sent by the client. - *

+ *
*

  * @RequestLine("POST /servers HTTP/1.1")
  * ...
  * 
- *

+ *
* Note: Query params do not overwrite each other. All queries with the same name will * be included in the request. - *

Relationship to JAXRS

- *

+ *

Relationship to JAXRS
+ *
* The following two forms are identical. - *

+ *
* Feign: *

  * @RequestLine("GET /servers/{serverId}?count={count}")
  * void get(@Named("serverId") String serverId, @Named("count") int count);
  * ...
  * 
- *

+ *
* JAX-RS: *

  * @GET @Path("/servers/{serverId}")
diff --git a/feign-core/src/main/java/feign/RequestTemplate.java b/feign-core/src/main/java/feign/RequestTemplate.java
index 5f085bee3b..5e49c45e43 100644
--- a/feign-core/src/main/java/feign/RequestTemplate.java
+++ b/feign-core/src/main/java/feign/RequestTemplate.java
@@ -37,9 +37,9 @@
 
 /**
  * Builds a request to an http target. Not thread safe.
- * 

- *

relationship to JAXRS 2.0

- *

+ *
+ *

relationship to JAXRS 2.0
+ *
* A combination of {@code javax.ws.rs.client.WebTarget} and * {@code javax.ws.rs.client.Invocation.Builder}, ensuring you can modify any * part of the request. However, this object is mutable, so needs to be guarded @@ -74,10 +74,10 @@ public RequestTemplate(RequestTemplate toCopy) { /** * Targets a template to this target, adding the {@link #url() base url} and * any authentication headers. - *

- *

+ *
+ *
* For example: - *

+ *
*

    * public Request apply(RequestTemplate input) {
    *     input.insert(0, url());
@@ -85,9 +85,9 @@ public RequestTemplate(RequestTemplate toCopy) {
    *     return input.asRequest();
    * }
    * 
- *

- *

relationship to JAXRS 2.0

- *

+ *
+ *

relationship to JAXRS 2.0
+ *
* This call is similar to * {@code javax.ws.rs.client.WebTarget.resolveTemplates(templateValues, true)} * , except that the template values apply to any part of the request, not @@ -151,7 +151,7 @@ private static String urlEncode(Object arg) { /** * Expands a {@code template}, such as {@code username}, using the {@code variables} supplied. Any unresolved * parameters will remain. - *

+ *
* Note that if you'd like curly braces literally in the {@code template}, * urlencode them first. * @@ -227,16 +227,16 @@ public String url() { /** * Replaces queries with the specified {@code configKey} with url decoded * {@code values} supplied. - *

+ *
* When the {@code value} is {@code null}, all queries with the {@code configKey} * are removed. - *

- *

relationship to JAXRS 2.0

- *

+ *
+ *

relationship to JAXRS 2.0
+ *
* Like {@code WebTarget.query}, except the values can be templatized. - *

+ *
* ex. - *

+ *
*

    * template.query("Signature", "{signature}");
    * 
@@ -274,13 +274,13 @@ private String encodeIfNotVariable(String in) { /** * Replaces all existing queries with the newly supplied url decoded * queries. - *

- *

relationship to JAXRS 2.0

- *

+ *
+ *

relationship to JAXRS 2.0
+ *
* Like {@code WebTarget.queries}, except the values can be templatized. - *

+ *
* ex. - *

+ *
*

    * template.queries(ImmutableMultimap.of("Signature", "{signature}"));
    * 
@@ -323,17 +323,17 @@ public Map> queries() { /** * Replaces headers with the specified {@code configKey} with the * {@code values} supplied. - *

+ *
* When the {@code value} is {@code null}, all headers with the {@code configKey} * are removed. - *

- *

relationship to JAXRS 2.0

- *

+ *
+ *

relationship to JAXRS 2.0
+ *
* Like {@code WebTarget.queries} and {@code javax.ws.rs.client.Invocation.Builder.header}, * except the values can be templatized. - *

+ *
* ex. - *

+ *
*

    * template.query("X-Application-Version", "{version}");
    * 
@@ -364,14 +364,14 @@ public RequestTemplate header(String configKey, Iterable values) { /** * Replaces all existing headers with the newly supplied headers. - *

- *

relationship to JAXRS 2.0

- *

+ *
+ *

relationship to JAXRS 2.0
+ *
* Like {@code Invocation.Builder.headers(MultivaluedMap)}, except the * values can be templatized. - *

+ *
* ex. - *

+ *
*

    * template.headers(ImmutableMultimap.of("X-Application-Version", "{version}"));
    * 
@@ -399,7 +399,7 @@ public Map> headers() { /** * replaces the {@link feign.Util#CONTENT_LENGTH} header. - *

+ *
* Usually populated by {@link feign.codec.BodyEncoder} or {@link feign.codec.FormEncoder} * * @see Request#body() diff --git a/feign-core/src/main/java/feign/Response.java b/feign-core/src/main/java/feign/Response.java index 2380010655..6baa2b8426 100644 --- a/feign-core/src/main/java/feign/Response.java +++ b/feign-core/src/main/java/feign/Response.java @@ -61,7 +61,7 @@ private Response(int status, String reason, Map> head /** * status code. ex {@code 200} * - * @see + * See rfc2616 */ public int status() { return status; @@ -86,9 +86,9 @@ public interface Body extends Closeable { /** * length in bytes, if known. Null if not. - *

- *

Note

This is an integer as most implementations cannot do - * bodies > 2GB. Moreover, the scope of this interface doesn't include + *
+ *

Note
This is an integer as most implementations cannot do + * bodies greater than 2GB. Moreover, the scope of this interface doesn't include * large bodies. */ Integer length(); diff --git a/feign-core/src/main/java/feign/Retryer.java b/feign-core/src/main/java/feign/Retryer.java index cad03c2877..b6cafe5db8 100644 --- a/feign-core/src/main/java/feign/Retryer.java +++ b/feign-core/src/main/java/feign/Retryer.java @@ -79,7 +79,7 @@ public void continueOrPropagate(RetryableException e) { /** * Calculates the time interval to a retry attempt. - *

+ *
* The interval increases exponentially with each attempt, at a rate of * nextInterval *= 1.5 (where 1.5 is the backoff factor), to the maximum * interval. diff --git a/feign-core/src/main/java/feign/Target.java b/feign-core/src/main/java/feign/Target.java index 70c5a7f367..d489a10cfe 100644 --- a/feign-core/src/main/java/feign/Target.java +++ b/feign-core/src/main/java/feign/Target.java @@ -21,8 +21,8 @@ import static feign.Util.emptyToNull; /** - *

relationship to JAXRS 2.0

- *

+ *

relationship to JAXRS 2.0
+ *
* Similar to {@code javax.ws.rs.client.WebTarget}, as it produces requests. * However, {@link RequestTemplate} is a closer match to {@code WebTarget}. * @@ -41,10 +41,10 @@ public interface Target { /** * Targets a template to this target, adding the {@link #url() base url} and * any authentication headers. - *

- *

+ *
+ *
* For example: - *

+ *
*

    * public Request apply(RequestTemplate input) {
    *     input.insert(0, url());
@@ -52,9 +52,9 @@ public interface Target {
    *     return input.asRequest();
    * }
    * 
- *

- *

relationship to JAXRS 2.0

- *

+ *
+ *

relationship to JAXRS 2.0
+ *
* This call is similar to {@code javax.ws.rs.client.WebTarget.request()}, * except that we expect transient, but necessary decoration to be applied * on invocation. diff --git a/feign-core/src/main/java/feign/codec/BodyEncoder.java b/feign-core/src/main/java/feign/codec/BodyEncoder.java index 6631d3e6a3..74f7c026eb 100644 --- a/feign-core/src/main/java/feign/codec/BodyEncoder.java +++ b/feign-core/src/main/java/feign/codec/BodyEncoder.java @@ -21,9 +21,9 @@ public interface BodyEncoder { /** * Converts objects to an appropriate representation. Can affect any part of * {@link RequestTemplate}. - *

+ *
* Ex. - *

+ *
*

    * public class GsonEncoder implements BodyEncoder {
    *   private final Gson gson;
@@ -38,10 +38,10 @@ public interface BodyEncoder {
    *   }
    * }
    * 
- *

+ *
* If a parameter has no {@code *Param} annotation, it is passed to this * method. - *

+ *
*

    * @POST
    * @Path("/")
diff --git a/feign-core/src/main/java/feign/codec/Decoder.java b/feign-core/src/main/java/feign/codec/Decoder.java
index 9eac156daf..18244a4795 100644
--- a/feign-core/src/main/java/feign/codec/Decoder.java
+++ b/feign-core/src/main/java/feign/codec/Decoder.java
@@ -24,9 +24,9 @@
 /**
  * Decodes an HTTP response into a given type. Invoked when
  * {@link Response#status()} is in the 2xx range.
- * 

+ *
* Ex. - *

+ *
*

  * public class GsonDecoder extends Decoder {
  *   private final Gson gson;
@@ -41,9 +41,9 @@
  *   }
  * }
  * 
- *

- *

Error handling

- *

+ *
+ *

Error handling
+ *
* Responses where {@link Response#status()} is not in the 2xx range are * classified as errors, addressed by the {@link ErrorDecoder}. That said, * certain RPC apis return errors defined in the {@link Response#body()} even on diff --git a/feign-core/src/main/java/feign/codec/Decoders.java b/feign-core/src/main/java/feign/codec/Decoders.java index 56e85981b8..39a1ac98b0 100644 --- a/feign-core/src/main/java/feign/codec/Decoders.java +++ b/feign-core/src/main/java/feign/codec/Decoders.java @@ -29,9 +29,9 @@ /** * Static utility methods pertaining to {@code Decoder} instances. - *

- *

Pattern Decoders

- *

+ *
+ *

Pattern Decoders
+ *
* Pattern decoders typically require less initialization, dependencies, and * code than reflective decoders, but not can be awkward to those unfamiliar * with regex. Typical use of pattern decoders is to grab a single field from an @@ -54,9 +54,9 @@ public interface ApplyFirstGroup { /** * The first match group is applied to {@code applyGroups} and result * returned. If no matches are found, the response is null; - *

+ *
* Ex. to pull the first interesting element from an xml response: - *

+ *
*

    * decodeFirstDirPoolID = transformFirstGroup("<DirPoolID[ˆ>]*>([ˆ<]+)</DirPoolID>", ToLong.INSTANCE);
    * 
@@ -83,10 +83,10 @@ public Object decode(String methodKey, Reader reader, Type type) throws Throwabl /** * shortcut for {@link Decoders#transformFirstGroup(String, ApplyFirstGroup)} when * {@code String} is the type you are decoding into. - *

- *

+ *
+ *
* Ex. to pull the first interesting element from an xml response: - *

+ *
*

    * decodeFirstDirPoolID = firstGroup("<DirPoolID[ˆ>]*>([ˆ<]+)</DirPoolID>");
    * 
@@ -99,10 +99,10 @@ public static Decoder firstGroup(String pattern) { * On the each find the first match group is applied to * {@code applyFirstGroup} and added to the list returned. If no matches are * found, the response is an empty list; - *

+ *
* Ex. to pull a list zones constructed from http paths starting with * {@code /Rest/Zone/}: - *

+ *
*

    * decodeListOfZones = transformEachFirstGroup("/REST/Zone/([ˆ/]+)/", ToZone.INSTANCE);
    * 
@@ -131,10 +131,10 @@ public List decode(String methodKey, Reader reader, Type type) throws Throwab /** * shortcut for {@link Decoders#transformEachFirstGroup(String, ApplyFirstGroup)} * when {@code List} is the type you are decoding into. - *

+ *
* Ex. to pull a list zones names, which are http paths starting with * {@code /Rest/Zone/}: - *

+ *
*

    * decodeListOfZonesNames = eachFirstGroup("/REST/Zone/([ˆ/]+)/");
    * 
diff --git a/feign-core/src/main/java/feign/codec/ErrorDecoder.java b/feign-core/src/main/java/feign/codec/ErrorDecoder.java index e19c5f005d..31d163ebf4 100644 --- a/feign-core/src/main/java/feign/codec/ErrorDecoder.java +++ b/feign-core/src/main/java/feign/codec/ErrorDecoder.java @@ -38,9 +38,9 @@ * fallback to a default value. Falling back to null on * {@link Response#status() status 404}, or converting out to a throttle * exception are examples of this in use. - *

+ *
* Ex. - *

+ *
*

  * class IllegalArgumentExceptionOn404Decoder extends ErrorDecoder {
  *
@@ -64,8 +64,7 @@ public interface ErrorDecoder {
    * {@link RetryableException}
    *
    * @param methodKey {@link feign.Feign#configKey} of the java method that invoked the request.  ex. {@code IAM#getUser()}
-   * @param response  HTTP response where {@link Response#status() status} >=
-   *                  {@code 300}.
+   * @param response  HTTP response where {@link Response#status() status} is greater than or equal to {@code 300}.
    * @param type      Target object type.
    * @return instance of {@code type}
    * @throws Throwable IOException, if there was a network error reading the
@@ -92,10 +91,10 @@ public Object decode(String methodKey, Response response, Type type) throws Thro
   /**
    * Decodes a {@link feign.Util#RETRY_AFTER} header into an absolute date,
    * if possible.
-   *
-   * @see Retry-After
-   *      format
+   * 
+ * See Retry-After + * format */ static class RetryAfterDecoder { static final DateFormat RFC822_FORMAT = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", US); diff --git a/feign-core/src/main/java/feign/codec/FormEncoder.java b/feign-core/src/main/java/feign/codec/FormEncoder.java index 3d3ab1e828..381f80c2d8 100644 --- a/feign-core/src/main/java/feign/codec/FormEncoder.java +++ b/feign-core/src/main/java/feign/codec/FormEncoder.java @@ -23,10 +23,10 @@ public interface FormEncoder { /** * FormParam encoding - *

+ *
* If any parameters are found in {@link feign.MethodMetadata#formParams()}, they will be * collected and passed as {code formParams} - *

+ *
*

    * @POST
    * @Path("/")
diff --git a/feign-core/src/test/java/feign/DefaultContractTest.java b/feign-core/src/test/java/feign/DefaultContractTest.java
index 3642bb0604..672b1f8773 100644
--- a/feign-core/src/test/java/feign/DefaultContractTest.java
+++ b/feign-core/src/test/java/feign/DefaultContractTest.java
@@ -20,14 +20,14 @@
 
 import org.testng.annotations.Test;
 
-import java.lang.annotation.*;
 import java.net.URI;
 
 import javax.inject.Named;
 
 import static feign.Util.CONTENT_TYPE;
 import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.*;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNull;
 import static org.testng.Assert.assertTrue;
 
 /**
diff --git a/feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java b/feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java
index 8aaa24ed36..40a4370c8b 100644
--- a/feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java
+++ b/feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java
@@ -26,7 +26,6 @@
 import java.lang.annotation.Target;
 import java.net.URI;
 
-import javax.inject.Named;
 import javax.ws.rs.DELETE;
 import javax.ws.rs.FormParam;
 import javax.ws.rs.GET;
@@ -41,7 +40,6 @@
 
 import feign.Body;
 import feign.MethodMetadata;
-import feign.RequestLine;
 import feign.Response;
 
 import static feign.Util.CONTENT_TYPE;
diff --git a/feign-ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java b/feign-ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java
index e7bcadc42e..c10aa5e6b3 100644
--- a/feign-ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java
+++ b/feign-ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java
@@ -33,7 +33,7 @@
 /**
  * Basic integration for {@link com.netflix.loadbalancer.ILoadBalancer loadbalancer-aware} targets.
  * Using this will enable dynamic url discovery via ribbon including incrementing server request counts.
- * 

+ *
* Ex. *

  * MyService api = Feign.create(LoadBalancingTarget.create(MyService.class, "http://myAppProd"))
diff --git a/feign-ribbon/src/main/java/feign/ribbon/RibbonModule.java b/feign-ribbon/src/main/java/feign/ribbon/RibbonModule.java
index aadc0d68e6..9054d10139 100644
--- a/feign-ribbon/src/main/java/feign/ribbon/RibbonModule.java
+++ b/feign-ribbon/src/main/java/feign/ribbon/RibbonModule.java
@@ -36,11 +36,11 @@
 /**
  * Adding this module will override URL resolution of {@link feign.Client Feign's client},
  * adding smart routing and resiliency capabilities provided by Ribbon.
- * 

+ *
* When using this, ensure the {@link feign.Target#url()} is set to as {@code http://clientName} * or {@code https://clientName}. {@link com.netflix.client.config.IClientConfig#getClientName() clientName} * will lookup the real url and port of your service dynamically. - *

+ *
* Ex. *

  * MyService api = Feign.create(MyService.class, "http://myAppProd", new RibbonModule());

From 54325443075894871e41b65e109ad1cf52d47fa0 Mon Sep 17 00:00:00 2001
From: adriancole 
Date: Mon, 1 Jul 2013 09:50:07 -0700
Subject: [PATCH 056/179] bump master to 3.0.0-SNAPSHOT

---
 gradle.properties | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gradle.properties b/gradle.properties
index 07ff68b985..cd92d6b085 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1 +1 @@
-version=2.0.0-SNAPSHOT
+version=3.0.0-SNAPSHOT

From 220ab36637070a01346240196a856427e8757737 Mon Sep 17 00:00:00 2001
From: adriancole 
Date: Mon, 1 Jul 2013 10:28:59 -0700
Subject: [PATCH 057/179] update example to use feign 2.0.0 syntax

---
 examples/feign-example-cli/build.gradle       |  2 +-
 .../java/feign/example/cli/GitHubExample.java | 21 ++++++++++---------
 2 files changed, 12 insertions(+), 11 deletions(-)

diff --git a/examples/feign-example-cli/build.gradle b/examples/feign-example-cli/build.gradle
index a9c44cab38..1a5882372d 100644
--- a/examples/feign-example-cli/build.gradle
+++ b/examples/feign-example-cli/build.gradle
@@ -1,7 +1,7 @@
 apply plugin: 'java'
 
 dependencies {
-  compile  'com.netflix.feign:feign-core:1.1.1'
+  compile  'com.netflix.feign:feign-core:2.0.0'
   compile  'com.google.code.gson:gson:2.2.4'
   provided 'com.squareup.dagger:dagger-compiler:1.0.1'
 }
diff --git a/examples/feign-example-cli/src/main/java/feign/example/cli/GitHubExample.java b/examples/feign-example-cli/src/main/java/feign/example/cli/GitHubExample.java
index 3b7657c4fd..48597d7e54 100644
--- a/examples/feign-example-cli/src/main/java/feign/example/cli/GitHubExample.java
+++ b/examples/feign-example-cli/src/main/java/feign/example/cli/GitHubExample.java
@@ -15,22 +15,21 @@
  */
 package feign.example.cli;
 
-import com.google.common.collect.ImmutableMap;
-import com.google.common.reflect.TypeToken;
 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 javax.ws.rs.GET;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
 
 import dagger.Module;
 import dagger.Provides;
 import feign.Feign;
+import feign.RequestLine;
 import feign.codec.Decoder;
 
 /**
@@ -39,8 +38,8 @@
 public class GitHubExample {
 
   interface GitHub {
-    @GET @Path("/repos/{owner}/{repo}/contributors")
-    List contributors(@PathParam("owner") String owner, @PathParam("repo") String repo);
+    @RequestLine("GET /repos/{owner}/{repo}/contributors")
+    List contributors(@Named("owner") String owner, @Named("repo") String repo);
   }
 
   static class Contributor {
@@ -64,14 +63,16 @@ public static void main(String... args) {
   @Module(overrides = true, library = true)
   static class GsonModule {
     @Provides @Singleton Map decoders() {
-      return ImmutableMap.of("GitHub", jsonDecoder);
+      Map decoders = new LinkedHashMap();
+      decoders.put("GitHub", jsonDecoder);
+      return decoders;
     }
 
     final Decoder jsonDecoder = new Decoder() {
       Gson gson = new Gson();
 
-      @Override public Object decode(String methodKey, Reader reader, TypeToken type) {
-        return gson.fromJson(reader, type.getType());
+      @Override public Object decode(String methodKey, Reader reader, Type type) {
+        return gson.fromJson(reader, type);
       }
     };
   }

From 52b74c985dda3642189aaca62d75d2a9850f60c6 Mon Sep 17 00:00:00 2001
From: adriancole 
Date: Sat, 6 Jul 2013 17:10:31 -0700
Subject: [PATCH 058/179] cleaned up usage of util and removed unused URI
 parser

---
 .../src/main/java/feign/MethodHandler.java    |  7 ----
 feign-core/src/main/java/feign/Util.java      | 37 -------------------
 .../main/java/feign/codec/ErrorDecoder.java   | 10 ++++-
 .../test/java/feign/DefaultContractTest.java  |  3 +-
 .../feign/examples/AWSSignatureVersion4.java  |  3 +-
 .../main/java/feign/jaxrs/JAXRSModule.java    | 18 +++++++--
 .../java/feign/jaxrs/JAXRSContractTest.java   |  2 +-
 7 files changed, 27 insertions(+), 53 deletions(-)

diff --git a/feign-core/src/main/java/feign/MethodHandler.java b/feign-core/src/main/java/feign/MethodHandler.java
index 0761b927e2..149addf91e 100644
--- a/feign-core/src/main/java/feign/MethodHandler.java
+++ b/feign-core/src/main/java/feign/MethodHandler.java
@@ -17,7 +17,6 @@
 
 import java.io.IOException;
 import java.lang.reflect.Type;
-import java.net.URI;
 
 import javax.inject.Inject;
 import javax.inject.Provider;
@@ -28,9 +27,7 @@
 
 import static feign.FeignException.errorExecuting;
 import static feign.FeignException.errorReading;
-import static feign.Util.LOCATION;
 import static feign.Util.checkNotNull;
-import static feign.Util.firstOrNull;
 
 final class MethodHandler {
 
@@ -109,10 +106,6 @@ public Object executeAndDecode(String configKey, RequestTemplate template, Type
       if (response.status() >= 200 && response.status() < 300) {
         if (returnType.equals(Response.class)) {
           return response;
-        } else if (returnType == URI.class && response.body() == null) {
-          String location = firstOrNull(response.headers(), LOCATION);
-          if (location != null)
-            return URI.create(location);
         } else if (returnType == void.class) {
           return null;
         }
diff --git a/feign-core/src/main/java/feign/Util.java b/feign-core/src/main/java/feign/Util.java
index c001aed24d..57206e8dae 100644
--- a/feign-core/src/main/java/feign/Util.java
+++ b/feign-core/src/main/java/feign/Util.java
@@ -31,27 +31,10 @@ public class Util {
   private Util() { // no instances
   }
 
-  // feign.Util
-  /**
-   * The HTTP Accept header field name.
-   */
-  public static final String ACCEPT = "Accept";
   /**
    * The HTTP Content-Length header field name.
    */
   public static final String CONTENT_LENGTH = "Content-Length";
-  /**
-   * The HTTP Content-Type header field name.
-   */
-  public static final String CONTENT_TYPE = "Content-Type";
-  /**
-   * The HTTP Host header field name.
-   */
-  public static final String HOST = "Host";
-  /**
-   * The HTTP Location header field name.
-   */
-  public static final String LOCATION = "Location";
   /**
    * The HTTP Retry-After header field name.
    */
@@ -108,19 +91,6 @@ public static String emptyToNull(String string) {
     return string == null || string.isEmpty() ? null : string;
   }
 
-  public 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();
-  }
-
   /**
    * Adapted from {@code com.google.common.base.Strings#emptyToNull}.
    */
@@ -144,11 +114,4 @@ public static  T[] toArray(Iterable iterable, Class type) {
   public static  Collection valuesOrEmpty(Map> map, String key) {
     return map.containsKey(key) ? map.get(key) : Collections.emptyList();
   }
-
-  public static  T firstOrNull(Map> map, String key) {
-    if (map.containsKey(key) && !map.get(key).isEmpty()) {
-      return map.get(key).iterator().next();
-    }
-    return null;
-  }
 }
diff --git a/feign-core/src/main/java/feign/codec/ErrorDecoder.java b/feign-core/src/main/java/feign/codec/ErrorDecoder.java
index 31d163ebf4..08a75faaf0 100644
--- a/feign-core/src/main/java/feign/codec/ErrorDecoder.java
+++ b/feign-core/src/main/java/feign/codec/ErrorDecoder.java
@@ -19,7 +19,9 @@
 import java.text.DateFormat;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
+import java.util.Collection;
 import java.util.Date;
+import java.util.Map;
 
 import feign.FeignException;
 import feign.Response;
@@ -28,7 +30,6 @@
 import static feign.FeignException.errorStatus;
 import static feign.Util.RETRY_AFTER;
 import static feign.Util.checkNotNull;
-import static feign.Util.firstOrNull;
 import static java.util.Locale.US;
 import static java.util.concurrent.TimeUnit.NANOSECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
@@ -86,6 +87,13 @@ public Object decode(String methodKey, Response response, Type type) throws Thro
         throw new RetryableException(exception.getMessage(), exception, retryAfter);
       throw exception;
     }
+
+    private  T firstOrNull(Map> map, String key) {
+      if (map.containsKey(key) && !map.get(key).isEmpty()) {
+        return map.get(key).iterator().next();
+      }
+      return null;
+    }
   };
 
   /**
diff --git a/feign-core/src/test/java/feign/DefaultContractTest.java b/feign-core/src/test/java/feign/DefaultContractTest.java
index 672b1f8773..8fc0dc2b71 100644
--- a/feign-core/src/test/java/feign/DefaultContractTest.java
+++ b/feign-core/src/test/java/feign/DefaultContractTest.java
@@ -24,7 +24,6 @@
 
 import javax.inject.Named;
 
-import static feign.Util.CONTENT_TYPE;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertFalse;
 import static org.testng.Assert.assertNull;
@@ -142,7 +141,7 @@ interface BodyWithoutParameters {
 
   @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"));
+    assertEquals(md.template().headers().get("Content-Type"), ImmutableSet.of("application/xml"));
   }
 
   interface WithURIParam {
diff --git a/feign-core/src/test/java/feign/examples/AWSSignatureVersion4.java b/feign-core/src/test/java/feign/examples/AWSSignatureVersion4.java
index dedc5a63b7..40c83eda84 100644
--- a/feign-core/src/test/java/feign/examples/AWSSignatureVersion4.java
+++ b/feign-core/src/test/java/feign/examples/AWSSignatureVersion4.java
@@ -37,7 +37,6 @@
 import static com.google.common.collect.Iterables.transform;
 import static com.google.common.hash.Hashing.sha256;
 import static com.google.common.io.BaseEncoding.base16;
-import static feign.Util.HOST;
 import static feign.Util.UTF_8;
 
 // http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
@@ -54,7 +53,7 @@ public AWSSignatureVersion4(String accessKey, String secretKey) {
   }
 
   @Override public Request apply(RequestTemplate input) {
-    input.header(HOST, URI.create(input.url()).getHost());
+    input.header("Host", URI.create(input.url()).getHost());
     TreeMultimap sortedLowercaseHeaders = TreeMultimap.create();
     for (String key : input.headers().keySet()) {
       sortedLowercaseHeaders.putAll(trimToLowercase.apply(key),
diff --git a/feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java b/feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java
index 07d6b9399d..ae01579bc0 100644
--- a/feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java
+++ b/feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java
@@ -33,13 +33,12 @@
 import feign.Contract;
 import feign.MethodMetadata;
 
-import static feign.Util.ACCEPT;
-import static feign.Util.CONTENT_TYPE;
 import static feign.Util.checkState;
-import static feign.Util.join;
 
 @dagger.Module(library = true)
 public final class JAXRSModule {
+  static final String ACCEPT = "Accept";
+  static final String CONTENT_TYPE = "Content-Type";
 
   @Provides Contract provideContract() {
     return new JAXRSContract();
@@ -103,4 +102,17 @@ protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[
       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();
+  }
 }
diff --git a/feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java b/feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java
index 40a4370c8b..6f02f9f9f3 100644
--- a/feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java
+++ b/feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java
@@ -42,7 +42,7 @@
 import feign.MethodMetadata;
 import feign.Response;
 
-import static feign.Util.CONTENT_TYPE;
+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;

From 46be6bd33645239a736f1798be0e87331b8d64f1 Mon Sep 17 00:00:00 2001
From: adriancole 
Date: Sun, 7 Jul 2013 08:10:25 -0700
Subject: [PATCH 059/179] issue #9: fallback handling is differs between sync
 and observer responses; decouple from error handling

---
 CHANGES.md                                    |  3 ++
 .../src/main/java/feign/MethodHandler.java    |  2 +-
 .../main/java/feign/codec/ErrorDecoder.java   | 37 ++++++++-----------
 feign-core/src/test/java/feign/FeignTest.java |  6 +--
 .../feign/codec/DefaultErrorDecoderTest.java  |  6 +--
 5 files changed, 25 insertions(+), 29 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 1f5d7de19d..6d663579ee 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,6 @@
+### Version 3.0
+* decoupled ErrorDecoder from fallback handling
+
 ### Version 2.0.0
 * removes guava and jax-rs dependencies
 * adds JAX-RS integration
diff --git a/feign-core/src/main/java/feign/MethodHandler.java b/feign-core/src/main/java/feign/MethodHandler.java
index 149addf91e..7f6d9327db 100644
--- a/feign-core/src/main/java/feign/MethodHandler.java
+++ b/feign-core/src/main/java/feign/MethodHandler.java
@@ -111,7 +111,7 @@ public Object executeAndDecode(String configKey, RequestTemplate template, Type
         }
         return decoder.decode(configKey, response, returnType);
       } else {
-        return errorDecoder.decode(configKey, response, returnType);
+        throw errorDecoder.decode(configKey, response);
       }
     } catch (Throwable e) {
       ensureBodyClosed(response);
diff --git a/feign-core/src/main/java/feign/codec/ErrorDecoder.java b/feign-core/src/main/java/feign/codec/ErrorDecoder.java
index 08a75faaf0..169935042a 100644
--- a/feign-core/src/main/java/feign/codec/ErrorDecoder.java
+++ b/feign-core/src/main/java/feign/codec/ErrorDecoder.java
@@ -15,7 +15,6 @@
  */
 package feign.codec;
 
-import java.lang.reflect.Type;
 import java.text.DateFormat;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
@@ -35,9 +34,7 @@
 import static java.util.concurrent.TimeUnit.SECONDS;
 
 /**
- * Allows you to massage an exception into a application-specific one, or
- * fallback to a default value. Falling back to null on
- * {@link Response#status() status 404}, or converting out to a throttle
+ * Allows you to massage an exception into a application-specific one. Converting out to a throttle
  * exception are examples of this in use.
  * 
* Ex. @@ -45,12 +42,12 @@ *
  * class IllegalArgumentExceptionOn404Decoder extends ErrorDecoder {
  *
- *     @Override
- *     public Object decode(String methodKey, Response response, Type<?> type) throws Throwable {
+ *   @Override
+ *   public Exception decode(String methodKey, Response response) {
  *    if (response.status() == 404)
  *        throw new IllegalArgumentException("zone not found");
- *    return ErrorDecoder.DEFAULT.decode(request, response, type);
- *     }
+ *    return ErrorDecoder.DEFAULT.decode(methodKey, request, response);
+ *   }
  *
  * }
  * 
@@ -59,33 +56,29 @@ public interface ErrorDecoder { /** * Implement this method in order to decode an HTTP {@link Response} when - * {@link Response#status()} is not in the 2xx range. Please raise - * application-specific exceptions or return fallback values where possible. - * If your exception is retryable, wrap or subclass - * {@link RetryableException} + * {@link Response#status()} is not in the 2xx range. Please raise application-specific exceptions where possible. + * If your exception is retryable, wrap or subclass {@link RetryableException} * * @param methodKey {@link feign.Feign#configKey} of the java method that invoked the request. ex. {@code IAM#getUser()} * @param response HTTP response where {@link Response#status() status} is greater than or equal to {@code 300}. - * @param type Target object type. - * @return instance of {@code type} - * @throws Throwable IOException, if there was a network error reading the - * response or an application-specific exception decoded by the - * implementation. If the throwable is retryable, it should be - * wrapped, or a subtype of {@link RetryableException} + * @return Exception IOException, if there was a network error reading the + * response or an application-specific exception decoded by the + * implementation. If the throwable is retryable, it should be + * wrapped, or a subtype of {@link RetryableException} */ - public Object decode(String methodKey, Response response, Type type) throws Throwable; + public Exception decode(String methodKey, Response response); public static final ErrorDecoder DEFAULT = new ErrorDecoder() { private final RetryAfterDecoder retryAfterDecoder = new RetryAfterDecoder(); @Override - public Object decode(String methodKey, Response response, Type type) throws Throwable { + public Exception decode(String methodKey, Response response) { FeignException exception = errorStatus(methodKey, response); Date retryAfter = retryAfterDecoder.apply(firstOrNull(response.headers(), RETRY_AFTER)); if (retryAfter != null) - throw new RetryableException(exception.getMessage(), exception, retryAfter); - throw exception; + return new RetryableException(exception.getMessage(), exception, retryAfter); + return exception; } private T firstOrNull(Map> map, String key) { diff --git a/feign-core/src/test/java/feign/FeignTest.java b/feign-core/src/test/java/feign/FeignTest.java index 306de900fc..fd060f8a08 100644 --- a/feign-core/src/test/java/feign/FeignTest.java +++ b/feign-core/src/test/java/feign/FeignTest.java @@ -93,10 +93,10 @@ public void canOverrideErrorDecoderOnMethod() throws IOException, InterruptedExc return ImmutableMap.of("TestInterface#post()", new ErrorDecoder() { @Override - public Object decode(String methodKey, Response response, Type type) throws Throwable { + public Exception decode(String methodKey, Response response) { if (response.status() == 404) - throw new IllegalArgumentException("zone not found"); - return ErrorDecoder.DEFAULT.decode(methodKey, response, type); + return new IllegalArgumentException("zone not found"); + return ErrorDecoder.DEFAULT.decode(methodKey, response); } }); diff --git a/feign-core/src/test/java/feign/codec/DefaultErrorDecoderTest.java b/feign-core/src/test/java/feign/codec/DefaultErrorDecoderTest.java index 835b50c7af..bd3b17835f 100644 --- a/feign-core/src/test/java/feign/codec/DefaultErrorDecoderTest.java +++ b/feign-core/src/test/java/feign/codec/DefaultErrorDecoderTest.java @@ -34,7 +34,7 @@ public void throwsFeignException() throws Throwable { Response response = Response.create(500, "Internal server error", ImmutableMap.>of(), null); - ErrorDecoder.DEFAULT.decode("Service#foo()", response, void.class); + throw ErrorDecoder.DEFAULT.decode("Service#foo()", response); } @Test(expectedExceptions = FeignException.class, expectedExceptionsMessageRegExp = "status 500 reading Service#foo\\(\\); content:\nhello world") @@ -42,7 +42,7 @@ public void throwsFeignExceptionIncludingBody() throws Throwable { Response response = Response.create(500, "Internal server error", ImmutableMap.>of(), "hello world"); - ErrorDecoder.DEFAULT.decode("Service#foo()", response, void.class); + throw ErrorDecoder.DEFAULT.decode("Service#foo()", response); } @Test(expectedExceptions = RetryableException.class, expectedExceptionsMessageRegExp = "status 503 reading Service#foo\\(\\)") @@ -50,6 +50,6 @@ public void retryAfterHeaderThrowsRetryableException() throws Throwable { Response response = Response.create(503, "Service Unavailable", ImmutableMultimap.of(RETRY_AFTER, "Sat, 1 Jan 2000 00:00:00 GMT").asMap(), null); - ErrorDecoder.DEFAULT.decode("Service#foo()", response, void.class); + throw ErrorDecoder.DEFAULT.decode("Service#foo()", response); } } From 4b8d6c326ccc214237064b5daa18ec9234afeb2a Mon Sep 17 00:00:00 2001 From: adriancole Date: Mon, 8 Jul 2013 08:07:07 -0700 Subject: [PATCH 060/179] centralize logic that ensures response body is closed --- feign-core/src/main/java/feign/MethodHandler.java | 12 ++---------- feign-core/src/main/java/feign/Util.java | 10 ++++++++++ feign-core/src/main/java/feign/Wire.java | 6 ++---- feign-core/src/main/java/feign/codec/Decoder.java | 7 +++---- .../src/main/java/feign/codec/ToStringDecoder.java | 7 +++---- 5 files changed, 20 insertions(+), 22 deletions(-) diff --git a/feign-core/src/main/java/feign/MethodHandler.java b/feign-core/src/main/java/feign/MethodHandler.java index 7f6d9327db..a643179e29 100644 --- a/feign-core/src/main/java/feign/MethodHandler.java +++ b/feign-core/src/main/java/feign/MethodHandler.java @@ -28,6 +28,7 @@ import static feign.FeignException.errorExecuting; import static feign.FeignException.errorReading; import static feign.Util.checkNotNull; +import static feign.Util.ensureClosed; final class MethodHandler { @@ -114,22 +115,13 @@ public Object executeAndDecode(String configKey, RequestTemplate template, Type throw errorDecoder.decode(configKey, response); } } catch (Throwable e) { - ensureBodyClosed(response); + ensureClosed(response.body()); if (IOException.class.isInstance(e)) throw errorReading(request, response, IOException.class.cast(e)); throw e; } } - private void ensureBodyClosed(Response response) { - if (response.body() != null) { - try { - response.body().close(); - } catch (IOException ignored) { // NOPMD - } - } - } - private Response execute(Request request) { try { return client.execute(request, options); diff --git a/feign-core/src/main/java/feign/Util.java b/feign-core/src/main/java/feign/Util.java index 57206e8dae..2d55d7e8b9 100644 --- a/feign-core/src/main/java/feign/Util.java +++ b/feign-core/src/main/java/feign/Util.java @@ -15,6 +15,7 @@ */ package feign; +import java.io.IOException; import java.lang.reflect.Array; import java.nio.charset.Charset; import java.util.ArrayList; @@ -114,4 +115,13 @@ public static T[] toArray(Iterable iterable, Class type) { public static Collection valuesOrEmpty(Map> map, String key) { return map.containsKey(key) ? map.get(key) : Collections.emptyList(); } + + public static void ensureClosed(Response.Body body) { + if (body != null) { + try { + body.close(); + } catch (IOException ignored) { // NOPMD + } + } + } } diff --git a/feign-core/src/main/java/feign/Wire.java b/feign-core/src/main/java/feign/Wire.java index 2c054476b8..fda8fae75f 100644 --- a/feign-core/src/main/java/feign/Wire.java +++ b/feign-core/src/main/java/feign/Wire.java @@ -25,6 +25,7 @@ import java.util.logging.Logger; import java.util.logging.SimpleFormatter; +import static feign.Util.ensureClosed; import static feign.Util.valuesOrEmpty; /* Writes http headers and body. Plumb to your favorite log impl. */ @@ -138,10 +139,7 @@ Response wireAndRebufferResponse(Target target, Response response) throws IOE } return Response.create(response.status(), response.reason(), response.headers(), buffered.toString()); } finally { - try { - body.close(); - } catch (IOException suppressed) { // NOPMD - } + ensureClosed(response.body()); } } return response; diff --git a/feign-core/src/main/java/feign/codec/Decoder.java b/feign-core/src/main/java/feign/codec/Decoder.java index 18244a4795..5f0b243fbe 100644 --- a/feign-core/src/main/java/feign/codec/Decoder.java +++ b/feign-core/src/main/java/feign/codec/Decoder.java @@ -21,6 +21,8 @@ import feign.Response; +import static feign.Util.ensureClosed; + /** * Decodes an HTTP response into a given type. Invoked when * {@link Response#status()} is in the 2xx range. @@ -73,10 +75,7 @@ public Object decode(String methodKey, Response response, Type type) throws Thro try { return decode(methodKey, reader, type); } finally { - try { - reader.close(); - } catch (IOException suppressed) { // NOPMD - } + ensureClosed(body); } } diff --git a/feign-core/src/main/java/feign/codec/ToStringDecoder.java b/feign-core/src/main/java/feign/codec/ToStringDecoder.java index b1ca2ab55b..b9b43774d5 100644 --- a/feign-core/src/main/java/feign/codec/ToStringDecoder.java +++ b/feign-core/src/main/java/feign/codec/ToStringDecoder.java @@ -22,6 +22,8 @@ import feign.Response; +import static feign.Util.ensureClosed; + /** * Adapted from {@code com.google.common.io.CharStreams.toString()}. */ @@ -38,10 +40,7 @@ public Object decode(String methodKey, Response response, Type type) throws IOEx try { return decode(methodKey, reader, type); } finally { - try { - reader.close(); - } catch (IOException suppressed) { // NOPMD - } + ensureClosed(body); } } From 02dd27ef31ef161cefaf7bcf646bc396ecd16b08 Mon Sep 17 00:00:00 2001 From: adriancole Date: Mon, 8 Jul 2013 12:19:12 -0700 Subject: [PATCH 061/179] refactor MethodHandler to be extensible --- .../src/main/java/feign/MethodHandler.java | 87 ++++++++++--------- 1 file changed, 47 insertions(+), 40 deletions(-) diff --git a/feign-core/src/main/java/feign/MethodHandler.java b/feign-core/src/main/java/feign/MethodHandler.java index a643179e29..9d71170d8c 100644 --- a/feign-core/src/main/java/feign/MethodHandler.java +++ b/feign-core/src/main/java/feign/MethodHandler.java @@ -16,7 +16,6 @@ package feign; import java.io.IOException; -import java.lang.reflect.Type; import javax.inject.Inject; import javax.inject.Provider; @@ -30,7 +29,7 @@ import static feign.Util.checkNotNull; import static feign.Util.ensureClosed; -final class MethodHandler { +abstract class MethodHandler { /** * Those using guava will implement as {@code Function}. @@ -53,25 +52,41 @@ static class Factory { public MethodHandler create(Target target, MethodMetadata md, BuildTemplateFromArgs buildTemplateFromArgs, Options options, Decoder decoder, ErrorDecoder errorDecoder) { - return new MethodHandler(target, client, retryer, wire, md, buildTemplateFromArgs, options, decoder, errorDecoder); + return new SynchronousMethodHandler(target, client, retryer, wire, md, buildTemplateFromArgs, options, decoder, errorDecoder); } } - private final MethodMetadata metadata; - private final Target target; - private final Client client; - private final Provider retryer; - private final Wire wire; - - private final BuildTemplateFromArgs buildTemplateFromArgs; - private final Options options; - private final Decoder decoder; - private final ErrorDecoder errorDecoder; - - // cannot inject wildcards in dagger - @SuppressWarnings("rawtypes") - private MethodHandler(Target target, Client client, Provider retryer, Wire wire, MethodMetadata metadata, - BuildTemplateFromArgs buildTemplateFromArgs, Options options, Decoder decoder, ErrorDecoder errorDecoder) { + static final class SynchronousMethodHandler extends MethodHandler { + private final Decoder decoder; + + private SynchronousMethodHandler(Target target, Client client, Provider retryer, Wire wire, MethodMetadata metadata, + BuildTemplateFromArgs buildTemplateFromArgs, Options options, Decoder decoder, ErrorDecoder errorDecoder) { + super(target, client, retryer, wire, metadata, buildTemplateFromArgs, options, errorDecoder); + this.decoder = checkNotNull(decoder, "decoder for %s", target); + } + + @Override protected Object decode(Object[] argv, Response response) throws Throwable { + if (metadata.returnType().equals(Response.class)) { + return response; + } else if (metadata.returnType() == void.class) { + return null; + } + return decoder.decode(metadata.configKey(), response, metadata.returnType()); + } + } + + protected final MethodMetadata metadata; + protected final Target target; + protected final Client client; + protected final Provider retryer; + protected final Wire wire; + + protected final BuildTemplateFromArgs buildTemplateFromArgs; + protected final Options options; + protected final ErrorDecoder errorDecoder; + + private MethodHandler(Target target, Client client, Provider retryer, Wire wire, 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); @@ -79,7 +94,6 @@ private MethodHandler(Target target, Client client, Provider retryer, W this.metadata = checkNotNull(metadata, "metadata for %s", target); this.buildTemplateFromArgs = checkNotNull(buildTemplateFromArgs, "metadata for %s", target); this.options = checkNotNull(options, "options for %s", target); - this.decoder = checkNotNull(decoder, "decoder for %s", target); this.errorDecoder = checkNotNull(errorDecoder, "errorDecoder for %s", target); } @@ -88,7 +102,7 @@ public Object invoke(Object[] argv) throws Throwable { Retryer retryer = this.retryer.get(); while (true) { try { - return executeAndDecode(metadata.configKey(), template, metadata.returnType()); + return executeAndDecode(argv, template); } catch (RetryableException e) { retryer.continueOrPropagate(e); continue; @@ -96,37 +110,30 @@ public Object invoke(Object[] argv) throws Throwable { } } - public Object executeAndDecode(String configKey, RequestTemplate template, Type returnType) + public Object executeAndDecode(Object[] argv, RequestTemplate template) throws Throwable { // create the request from a mutable copy of the input template. Request request = target.apply(new RequestTemplate(template)); wire.wireRequest(target, request); - Response response = execute(request); + Response response; + try { + response = client.execute(request, options); + } catch (IOException e) { + throw errorExecuting(request, e); + } try { response = wire.wireAndRebufferResponse(target, response); if (response.status() >= 200 && response.status() < 300) { - if (returnType.equals(Response.class)) { - return response; - } else if (returnType == void.class) { - return null; - } - return decoder.decode(configKey, response, returnType); + return decode(argv, response); } else { - throw errorDecoder.decode(configKey, response); + throw errorDecoder.decode(metadata.configKey(), response); } - } catch (Throwable e) { + } catch (IOException e) { + throw errorReading(request, response, e); + } finally { ensureClosed(response.body()); - if (IOException.class.isInstance(e)) - throw errorReading(request, response, IOException.class.cast(e)); - throw e; } } - private Response execute(Request request) { - try { - return client.execute(request, options); - } catch (IOException e) { - throw errorExecuting(request, e); - } - } + protected abstract Object decode(Object[] argv, Response response) throws Throwable; } From 6523d560166439a0d8fbf941559f670ed017e350 Mon Sep 17 00:00:00 2001 From: adriancole Date: Fri, 12 Jul 2013 08:52:55 -0700 Subject: [PATCH 062/179] Decoders can throw checked exceptions, but needn't declare Throwable --- CHANGES.md | 1 + feign-core/src/main/java/feign/Client.java | 2 -- feign-core/src/main/java/feign/FeignException.java | 4 ++-- feign-core/src/main/java/feign/ReflectiveFeign.java | 6 +++--- feign-core/src/main/java/feign/Util.java | 1 + feign-core/src/main/java/feign/codec/Decoder.java | 8 +++++--- feign-core/src/main/java/feign/codec/Decoders.java | 9 +++++---- .../codec/{ToStringDecoder.java => StringDecoder.java} | 2 +- feign-core/src/test/java/feign/FeignTest.java | 6 +++--- 9 files changed, 21 insertions(+), 18 deletions(-) rename feign-core/src/main/java/feign/codec/{ToStringDecoder.java => StringDecoder.java} (97%) diff --git a/CHANGES.md b/CHANGES.md index 6d663579ee..879d0cd7bb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ ### Version 3.0 * decoupled ErrorDecoder from fallback handling +* Decoders can throw checked exceptions, but needn't declare Throwable ### Version 2.0.0 * removes guava and jax-rs dependencies diff --git a/feign-core/src/main/java/feign/Client.java b/feign-core/src/main/java/feign/Client.java index 2b7a1afefe..315ccd83e0 100644 --- a/feign-core/src/main/java/feign/Client.java +++ b/feign-core/src/main/java/feign/Client.java @@ -107,8 +107,6 @@ HttpURLConnection convertAndSend(Request request, Options options) throws IOExce return connection; } - private static final int BUF_SIZE = 0x800; // 2K chars (4K bytes) - Response convertResponse(HttpURLConnection connection) throws IOException { int status = connection.getResponseCode(); String reason = connection.getResponseMessage(); diff --git a/feign-core/src/main/java/feign/FeignException.java b/feign-core/src/main/java/feign/FeignException.java index bb4c6e61e2..a7607924ee 100644 --- a/feign-core/src/main/java/feign/FeignException.java +++ b/feign-core/src/main/java/feign/FeignException.java @@ -17,7 +17,7 @@ import java.io.IOException; -import feign.codec.ToStringDecoder; +import feign.codec.StringDecoder; import static java.lang.String.format; @@ -29,7 +29,7 @@ static FeignException errorReading(Request request, Response response, IOExcepti return new FeignException(format("%s %s %s", cause.getMessage(), request.method(), request.url(), 0), cause); } - private static final ToStringDecoder toString = new ToStringDecoder(); + private static final StringDecoder toString = new StringDecoder(); public static FeignException errorStatus(String methodKey, Response response) { String message = format("status %s reading %s", response.status(), methodKey); diff --git a/feign-core/src/main/java/feign/ReflectiveFeign.java b/feign-core/src/main/java/feign/ReflectiveFeign.java index c9c623932a..bc77f5e8b3 100644 --- a/feign-core/src/main/java/feign/ReflectiveFeign.java +++ b/feign-core/src/main/java/feign/ReflectiveFeign.java @@ -33,7 +33,7 @@ import feign.codec.Decoder; import feign.codec.ErrorDecoder; import feign.codec.FormEncoder; -import feign.codec.ToStringDecoder; +import feign.codec.StringDecoder; import static feign.Util.checkArgument; import static feign.Util.checkNotNull; @@ -52,7 +52,7 @@ public class ReflectiveFeign extends Feign { * creates an api binding to the {@code target}. As this invokes reflection, * care should be taken to cache the result. */ - @Override public T newInstance(Target target) { + @SuppressWarnings("unchecked") @Override public T newInstance(Target target) { Map nameToHandler = targetToHandlersByName.apply(target); Map methodToHandler = new LinkedHashMap(); for (Method method : target.type().getDeclaredMethods()) { @@ -143,7 +143,7 @@ public Map apply(Target key) { Decoder decoder = forMethodOrClass(decoders, md.configKey()); if (decoder == null && (md.returnType() == void.class || md.returnType() == Response.class)) { - decoder = new ToStringDecoder(); + decoder = new StringDecoder(); } if (decoder == null) { throw noConfig(md.configKey(), Decoder.class); diff --git a/feign-core/src/main/java/feign/Util.java b/feign-core/src/main/java/feign/Util.java index 2d55d7e8b9..edd6513d05 100644 --- a/feign-core/src/main/java/feign/Util.java +++ b/feign-core/src/main/java/feign/Util.java @@ -95,6 +95,7 @@ public static String emptyToNull(String string) { /** * Adapted from {@code com.google.common.base.Strings#emptyToNull}. */ + @SuppressWarnings("unchecked") public static T[] toArray(Iterable iterable, Class type) { Collection collection; if (iterable instanceof Collection) { diff --git a/feign-core/src/main/java/feign/codec/Decoder.java b/feign-core/src/main/java/feign/codec/Decoder.java index 5f0b243fbe..c21ecbd184 100644 --- a/feign-core/src/main/java/feign/codec/Decoder.java +++ b/feign-core/src/main/java/feign/codec/Decoder.java @@ -66,8 +66,9 @@ public abstract class Decoder { * @param type Target object type. * @return instance of {@code type} * @throws IOException if there was a network error reading the response. + * @throws Exception if the decoder threw a checked exception. */ - public Object decode(String methodKey, Response response, Type type) throws Throwable { + public Object decode(String methodKey, Response response, Type type) throws Exception { Response.Body body = response.body(); if (body == null) return null; @@ -89,7 +90,8 @@ public Object decode(String methodKey, Response response, Type type) throws Thro * manages resources. * @param type Target object type. * @return instance of {@code type} - * @throws Throwable will be propagated safely to the caller. + * @throws IOException will be propagated safely to the caller. + * @throws Exception if the decoder threw a checked exception. */ - public abstract Object decode(String methodKey, Reader reader, Type type) throws Throwable; + public abstract Object decode(String methodKey, Reader reader, Type type) throws Exception; } diff --git a/feign-core/src/main/java/feign/codec/Decoders.java b/feign-core/src/main/java/feign/codec/Decoders.java index 39a1ac98b0..d26831548e 100644 --- a/feign-core/src/main/java/feign/codec/Decoders.java +++ b/feign-core/src/main/java/feign/codec/Decoders.java @@ -15,6 +15,7 @@ */ package feign.codec; +import java.io.IOException; import java.io.Reader; import java.lang.reflect.Type; import java.util.ArrayList; @@ -66,7 +67,7 @@ public static Decoder transformFirstGroup(String pattern, final ApplyFirstGr checkNotNull(applyFirstGroup, "applyFirstGroup"); return new Decoder() { @Override - public Object decode(String methodKey, Reader reader, Type type) throws Throwable { + public Object decode(String methodKey, Reader reader, Type type) throws IOException { Matcher matcher = patternForMatcher.matcher(Decoders.toString(reader)); if (matcher.find()) { return applyFirstGroup.apply(matcher.group(1)); @@ -112,7 +113,7 @@ public static Decoder transformEachFirstGroup(String pattern, final ApplyFir checkNotNull(applyFirstGroup, "applyFirstGroup"); return new Decoder() { @Override - public List decode(String methodKey, Reader reader, Type type) throws Throwable { + public List decode(String methodKey, Reader reader, Type type) throws IOException { Matcher matcher = patternForMatcher.matcher(Decoders.toString(reader)); List result = new ArrayList(); while (matcher.find()) { @@ -143,11 +144,11 @@ public static Decoder eachFirstGroup(String pattern) { return transformEachFirstGroup(pattern, IDENTITY); } - private static String toString(Reader reader) throws Throwable { + private static String toString(Reader reader) throws IOException { return TO_STRING.decode(null, reader, null).toString(); } - private static final Decoder TO_STRING = new ToStringDecoder(); + private static final StringDecoder TO_STRING = new StringDecoder(); private static final ApplyFirstGroup IDENTITY = new ApplyFirstGroup() { @Override public String apply(String firstGroup) { diff --git a/feign-core/src/main/java/feign/codec/ToStringDecoder.java b/feign-core/src/main/java/feign/codec/StringDecoder.java similarity index 97% rename from feign-core/src/main/java/feign/codec/ToStringDecoder.java rename to feign-core/src/main/java/feign/codec/StringDecoder.java index b9b43774d5..2ad10a2ad0 100644 --- a/feign-core/src/main/java/feign/codec/ToStringDecoder.java +++ b/feign-core/src/main/java/feign/codec/StringDecoder.java @@ -27,7 +27,7 @@ /** * Adapted from {@code com.google.common.io.CharStreams.toString()}. */ -public class ToStringDecoder extends Decoder { +public class StringDecoder extends Decoder { private static final int BUF_SIZE = 0x800; // 2K chars (4K bytes) // overridden to throw only IOException diff --git a/feign-core/src/test/java/feign/FeignTest.java b/feign-core/src/test/java/feign/FeignTest.java index fd060f8a08..8adb8b67d2 100644 --- a/feign-core/src/test/java/feign/FeignTest.java +++ b/feign-core/src/test/java/feign/FeignTest.java @@ -36,7 +36,7 @@ import dagger.Provides; import feign.codec.Decoder; import feign.codec.ErrorDecoder; -import feign.codec.ToStringDecoder; +import feign.codec.StringDecoder; import static org.testng.Assert.assertEquals; @@ -58,7 +58,7 @@ static class Module { // until dagger supports real map binding, we need to recreate the // entire map, as opposed to overriding a single entry. @Provides @Singleton Map decoders() { - return ImmutableMap.of("TestInterface", new ToStringDecoder()); + return ImmutableMap.of("TestInterface", new StringDecoder()); } } } @@ -146,7 +146,7 @@ public void doesntRetryAfterResponseIsSent() throws IOException, InterruptedExce return ImmutableMap.of("TestInterface", new Decoder() { @Override - public Object decode(String methodKey, Reader reader, Type type) throws Throwable { + public Object decode(String methodKey, Reader reader, Type type) throws IOException { throw new IOException("error reading response"); } From b79e9de65faf3162cc7da0cfb5a25d196c7df055 Mon Sep 17 00:00:00 2001 From: adriancole Date: Fri, 12 Jul 2013 09:23:51 -0700 Subject: [PATCH 063/179] Decoders no longer read methodKey --- CHANGES.md | 1 + feign-core/src/main/java/feign/FeignException.java | 2 +- feign-core/src/main/java/feign/MethodHandler.java | 2 +- feign-core/src/main/java/feign/codec/Decoder.java | 13 +++++-------- feign-core/src/main/java/feign/codec/Decoders.java | 6 +++--- .../src/main/java/feign/codec/SAXDecoder.java | 3 +-- .../src/main/java/feign/codec/StringDecoder.java | 6 +++--- feign-core/src/test/java/feign/FeignTest.java | 2 +- .../src/test/java/feign/examples/GitHubExample.java | 5 ++--- .../java/feign/jaxrs/examples/GitHubExample.java | 2 +- 10 files changed, 19 insertions(+), 23 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 879d0cd7bb..4dd52ff543 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,7 @@ ### Version 3.0 * decoupled ErrorDecoder from fallback handling * Decoders can throw checked exceptions, but needn't declare Throwable +* Decoders no longer read methodKey ### Version 2.0.0 * removes guava and jax-rs dependencies diff --git a/feign-core/src/main/java/feign/FeignException.java b/feign-core/src/main/java/feign/FeignException.java index a7607924ee..7ceef0d13e 100644 --- a/feign-core/src/main/java/feign/FeignException.java +++ b/feign-core/src/main/java/feign/FeignException.java @@ -34,7 +34,7 @@ static FeignException errorReading(Request request, Response response, IOExcepti public static FeignException errorStatus(String methodKey, Response response) { String message = format("status %s reading %s", response.status(), methodKey); try { - Object body = toString.decode(methodKey, response, String.class); + Object body = toString.decode(response, String.class); if (body != null) { response = Response.create(response.status(), response.reason(), response.headers(), body.toString()); message += "; content:\n" + body; diff --git a/feign-core/src/main/java/feign/MethodHandler.java b/feign-core/src/main/java/feign/MethodHandler.java index 9d71170d8c..949bb4e7bf 100644 --- a/feign-core/src/main/java/feign/MethodHandler.java +++ b/feign-core/src/main/java/feign/MethodHandler.java @@ -71,7 +71,7 @@ private SynchronousMethodHandler(Target target, Client client, Provider Decoder transformFirstGroup(String pattern, final ApplyFirstGr checkNotNull(applyFirstGroup, "applyFirstGroup"); return new Decoder() { @Override - public Object decode(String methodKey, Reader reader, Type type) throws IOException { + public Object decode(Reader reader, Type type) throws IOException { Matcher matcher = patternForMatcher.matcher(Decoders.toString(reader)); if (matcher.find()) { return applyFirstGroup.apply(matcher.group(1)); @@ -113,7 +113,7 @@ public static Decoder transformEachFirstGroup(String pattern, final ApplyFir checkNotNull(applyFirstGroup, "applyFirstGroup"); return new Decoder() { @Override - public List decode(String methodKey, Reader reader, Type type) throws IOException { + public List decode(Reader reader, Type type) throws IOException { Matcher matcher = patternForMatcher.matcher(Decoders.toString(reader)); List result = new ArrayList(); while (matcher.find()) { @@ -145,7 +145,7 @@ public static Decoder eachFirstGroup(String pattern) { } private static String toString(Reader reader) throws IOException { - return TO_STRING.decode(null, reader, null).toString(); + return TO_STRING.decode(reader, null).toString(); } private static final StringDecoder TO_STRING = new StringDecoder(); diff --git a/feign-core/src/main/java/feign/codec/SAXDecoder.java b/feign-core/src/main/java/feign/codec/SAXDecoder.java index 36a84171eb..25cf8efc9c 100644 --- a/feign-core/src/main/java/feign/codec/SAXDecoder.java +++ b/feign-core/src/main/java/feign/codec/SAXDecoder.java @@ -50,8 +50,7 @@ protected SAXDecoder(SAXParserFactory factory) { } @Override - public Object decode(String methodKey, Reader reader, Type type) throws IOException, SAXException, - ParserConfigurationException { + public Object decode(Reader reader, Type type) throws IOException, SAXException, ParserConfigurationException { ContentHandlerWithResult handler = typeToNewHandler(type); checkState(handler != null, "%s returned null for type %s", this, type); XMLReader xmlReader = factory.newSAXParser().getXMLReader(); diff --git a/feign-core/src/main/java/feign/codec/StringDecoder.java b/feign-core/src/main/java/feign/codec/StringDecoder.java index 2ad10a2ad0..3e34fc2b59 100644 --- a/feign-core/src/main/java/feign/codec/StringDecoder.java +++ b/feign-core/src/main/java/feign/codec/StringDecoder.java @@ -32,20 +32,20 @@ public class StringDecoder extends Decoder { // overridden to throw only IOException @Override - public Object decode(String methodKey, Response response, Type type) throws IOException { + public Object decode(Response response, Type type) throws IOException { Response.Body body = response.body(); if (body == null) return null; Reader reader = body.asReader(); try { - return decode(methodKey, reader, type); + return decode(reader, type); } finally { ensureClosed(body); } } @Override - public Object decode(String methodKey, Reader from, Type type) throws IOException { + public Object decode(Reader from, Type type) throws IOException { StringBuilder to = new StringBuilder(); CharBuffer buf = CharBuffer.allocate(BUF_SIZE); while (from.read(buf) != -1) { diff --git a/feign-core/src/test/java/feign/FeignTest.java b/feign-core/src/test/java/feign/FeignTest.java index 8adb8b67d2..bfb4eadeba 100644 --- a/feign-core/src/test/java/feign/FeignTest.java +++ b/feign-core/src/test/java/feign/FeignTest.java @@ -146,7 +146,7 @@ public void doesntRetryAfterResponseIsSent() throws IOException, InterruptedExce return ImmutableMap.of("TestInterface", new Decoder() { @Override - public Object decode(String methodKey, Reader reader, Type type) throws IOException { + public Object decode(Reader reader, Type type) throws IOException { throw new IOException("error reading response"); } diff --git a/feign-core/src/test/java/feign/examples/GitHubExample.java b/feign-core/src/test/java/feign/examples/GitHubExample.java index 93d7108048..61b317e7d9 100644 --- a/feign-core/src/test/java/feign/examples/GitHubExample.java +++ b/feign-core/src/test/java/feign/examples/GitHubExample.java @@ -76,7 +76,7 @@ static class GsonModule { final Decoder jsonDecoder = new Decoder() { Gson gson = new Gson(); - @Override public Object decode(String methodKey, Reader reader, Type type) { + @Override public Object decode(Reader reader, Type type) { return gson.fromJson(reader, type); } }; @@ -94,8 +94,7 @@ static class JacksonModule { final Decoder jsonDecoder = new Decoder() { ObjectMapper mapper = new ObjectMapper().disable(FAIL_ON_UNKNOWN_PROPERTIES).setVisibility(FIELD, ANY); - @Override public Object decode(String methodKey, Reader reader, final Type type) - throws JsonProcessingException, IOException { + @Override public Object decode(Reader reader, final Type type) throws JsonProcessingException, IOException { return mapper.readValue(reader, mapper.constructType(type)); } }; diff --git a/feign-jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java b/feign-jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java index 3499a85151..f8692bf54a 100644 --- a/feign-jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java +++ b/feign-jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java @@ -71,7 +71,7 @@ static class GitHubModule { final Decoder jsonDecoder = new Decoder() { Gson gson = new Gson(); - @Override public Object decode(String methodKey, Reader reader, Type type) { + @Override public Object decode(Reader reader, Type type) { return gson.fromJson(reader, type); } }; From 3aa54db0b2d6b6341d46ecc76062c6366525dde8 Mon Sep 17 00:00:00 2001 From: adriancole Date: Sun, 14 Jul 2013 15:43:20 -0700 Subject: [PATCH 064/179] fix duplicate binding error using jaxrs --- feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java b/feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java index ae01579bc0..9e766387a1 100644 --- a/feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java +++ b/feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java @@ -35,7 +35,7 @@ import static feign.Util.checkState; -@dagger.Module(library = true) +@dagger.Module(library = true, overrides = true) public final class JAXRSModule { static final String ACCEPT = "Accept"; static final String CONTENT_TYPE = "Content-Type"; From 2438a851a9caa365054515ebcbc8de1fd2075e58 Mon Sep 17 00:00:00 2001 From: adriancole Date: Mon, 15 Jul 2013 11:13:19 -0700 Subject: [PATCH 065/179] fix issue #16: Wire is now Logger, with configurable Logger.Level --- CHANGES.md | 1 + README.md | 12 +- feign-core/src/main/java/feign/Feign.java | 12 +- feign-core/src/main/java/feign/Logger.java | 197 ++++++++++++++++++ .../src/main/java/feign/MethodHandler.java | 48 +++-- feign-core/src/main/java/feign/Wire.java | 147 ------------- .../src/test/java/feign/LoggerTest.java | 137 ++++++++++++ 7 files changed, 385 insertions(+), 169 deletions(-) create mode 100644 feign-core/src/main/java/feign/Logger.java delete mode 100644 feign-core/src/main/java/feign/Wire.java create mode 100644 feign-core/src/test/java/feign/LoggerTest.java diff --git a/CHANGES.md b/CHANGES.md index 4dd52ff543..685f9893a0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,5 @@ ### Version 3.0 +* Wire is now Logger, with configurable Logger.Level. * decoupled ErrorDecoder from fallback handling * Decoders can throw checked exceptions, but needn't declare Throwable * Decoders no longer read methodKey diff --git a/README.md b/README.md index e63c3c0419..ea1622eba2 100644 --- a/README.md +++ b/README.md @@ -95,13 +95,17 @@ Almost all configuration of Feign is represented as Map bindings, where the key return ImmutableMap.of("GitHub", gsonDecoder); } ``` -#### Wire Logging -You can log the http messages going to and from the target by setting up a `Wire`. Here's the easiest way to do that: +#### Logging +You can log the http messages going to and from the target by setting up a `Logger`. Here's the easiest way to do that: ```java @Module(overrides = true) class Overrides { - @Provides @Singleton Wire provideWire() { - return new Wire.LoggingWire().appendToFile("logs/http-wire.log"); + @Provides @Singleton Logger.Level provideLoggerLevel() { + return Logger.Level.FULL; + } + + @Provides @Singleton Logger provideLogger() { + return new Logger.JavaLogger().appendToFile("logs/http.log"); } } GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonGitHubModule(), new Overrides()); diff --git a/feign-core/src/main/java/feign/Feign.java b/feign-core/src/main/java/feign/Feign.java index 7cfa35864e..867d24f79c 100644 --- a/feign-core/src/main/java/feign/Feign.java +++ b/feign-core/src/main/java/feign/Feign.java @@ -27,7 +27,7 @@ import dagger.Provides; import feign.Request.Options; import feign.Target.HardCodedTarget; -import feign.Wire.NoOpWire; +import feign.Logger.NoOpLogger; import feign.codec.BodyEncoder; import feign.codec.Decoder; import feign.codec.ErrorDecoder; @@ -80,6 +80,12 @@ public static ObjectGraph createObjectGraph(Object... modules) { @dagger.Module(complete = false, injects = Feign.class, library = true) public static class Defaults { + + @Provides + Logger.Level logLevel() { + return Logger.Level.NONE; + } + @Provides Contract contract() { return new Contract.DefaultContract(); } @@ -96,8 +102,8 @@ public static class Defaults { return new Retryer.Default(); } - @Provides Wire noOp() { - return new NoOpWire(); + @Provides Logger noOp() { + return new NoOpLogger(); } @Provides Map noOptions() { diff --git a/feign-core/src/main/java/feign/Logger.java b/feign-core/src/main/java/feign/Logger.java new file mode 100644 index 0000000000..96fdea456c --- /dev/null +++ b/feign-core/src/main/java/feign/Logger.java @@ -0,0 +1,197 @@ +/* + * 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.BufferedReader; +import java.io.IOException; +import java.io.Reader; +import java.text.SimpleDateFormat; +import java.util.logging.FileHandler; +import java.util.logging.LogRecord; +import java.util.logging.SimpleFormatter; + +import static feign.Util.UTF_8; +import static feign.Util.ensureClosed; +import static feign.Util.valuesOrEmpty; + +/** + * Simple logging abstraction for debug messages. Adapted from {@code retrofit.RestAdapter.Log}. + */ +public abstract class Logger { + + /** + * Controls the level of logging. + */ + public enum Level { + /** + * No logging. + */ + NONE, + /** + * Log only the request method and URL and the response status code and execution time. + */ + BASIC, + /** + * Log the basic information along with request and response headers. + */ + HEADERS, + /** + * Log the headers, body, and metadata for both requests and responses. + */ + FULL + } + + /** + * logs to the category {@link Logger} at {@link java.util.logging.Level#FINE}. + */ + 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); + } + } + + /** + * logs to the category {@link Logger} at {@link java.util.logging.Level#FINE}, if loggable. + */ + public static class JavaLogger extends Logger { + final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(Logger.class.getName()); + + @Override void logRequest(Target target, Level logLevel, Request request) { + if (logger.isLoggable(java.util.logging.Level.FINE)) { + super.logRequest(target, logLevel, request); + } + } + + @Override + Response logAndRebufferResponse(Target target, Level logLevel, Response response, long elapsedTime) throws IOException { + if (logger.isLoggable(java.util.logging.Level.FINE)) { + return super.logAndRebufferResponse(target, logLevel, response, elapsedTime); + } + return response; + } + + @Override protected void log(Target target, String format, Object... args) { + logger.fine(String.format(format, args)); + } + + /** + * helper that configures jul to sanely log messages. + */ + public JavaLogger appendToFile(String logfile) { + final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + logger.setLevel(java.util.logging.Level.FINE); + try { + FileHandler handler = new FileHandler(logfile, true); + handler.setFormatter(new SimpleFormatter() { + @Override + public String format(LogRecord record) { + String timestamp = sdf.format(new java.util.Date(record.getMillis())); // NOPMD + return String.format("%s %s%n", timestamp, record.getMessage()); // NOPMD + } + }); + logger.addHandler(handler); + } catch (IOException e) { + throw new IllegalStateException("Could not add file handler.", e); + } + return this; + } + } + + public static class NoOpLogger extends Logger { + @Override void logRequest(Target target, Level logLevel, Request request) { + } + + @Override + Response logAndRebufferResponse(Target target, Level logLevel, Response response, long elapsedTime) throws IOException { + return response; + } + + @Override + protected void log(Target target, String format, Object... args) { + } + } + + /** + * 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} + */ + protected abstract void log(Target target, String format, Object... args); + + void logRequest(Target target, Level logLevel, Request request) { + log(target, "---> %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); + } + } + + int bytes = 0; + 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(target, "---> 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); + 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); + } + } + + if (response.body() != null) { + if (logLevel.ordinal() >= Level.FULL.ordinal()) { + log(target, ""); // CRLF + } + + Reader body = response.body().asReader(); + try { + StringBuilder buffered = new StringBuilder(); + BufferedReader reader = new BufferedReader(body); + String line; + while ((line = reader.readLine()) != null) { + buffered.append(line); + if (logLevel.ordinal() >= Level.FULL.ordinal()) { + log(target, "%s", line); + } + } + String bodyAsString = buffered.toString(); + log(target, "<--- END HTTP (%s-byte body)", bodyAsString.getBytes(UTF_8).length); + return Response.create(response.status(), response.reason(), response.headers(), bodyAsString); + } finally { + ensureClosed(response.body()); + } + } + } + return response; + } +} diff --git a/feign-core/src/main/java/feign/MethodHandler.java b/feign-core/src/main/java/feign/MethodHandler.java index 949bb4e7bf..f58bf3a5de 100644 --- a/feign-core/src/main/java/feign/MethodHandler.java +++ b/feign-core/src/main/java/feign/MethodHandler.java @@ -16,6 +16,7 @@ package feign; import java.io.IOException; +import java.util.concurrent.TimeUnit; import javax.inject.Inject; import javax.inject.Provider; @@ -42,26 +43,31 @@ static class Factory { private final Client client; private final Provider retryer; - private final Wire wire; + private final Logger logger; + private final Logger.Level logLevel; - @Inject Factory(Client client, Provider retryer, Wire wire) { + @Inject Factory(Client client, Provider retryer, Logger logger, Logger.Level logLevel) { this.client = checkNotNull(client, "client"); this.retryer = checkNotNull(retryer, "retryer"); - this.wire = checkNotNull(wire, "wire"); + this.logger = checkNotNull(logger, "logger"); + this.logLevel = checkNotNull(logLevel, "logLevel"); } - public MethodHandler create(Target target, MethodMetadata md, - BuildTemplateFromArgs buildTemplateFromArgs, Options options, Decoder decoder, ErrorDecoder errorDecoder) { - return new SynchronousMethodHandler(target, client, retryer, wire, md, buildTemplateFromArgs, options, decoder, errorDecoder); + public MethodHandler create(Target target, MethodMetadata md, BuildTemplateFromArgs buildTemplateFromArgs, + Options options, Decoder decoder, ErrorDecoder errorDecoder) { + return new SynchronousMethodHandler(target, client, retryer, logger, logLevel, md, buildTemplateFromArgs, + options, decoder, errorDecoder); } } static final class SynchronousMethodHandler extends MethodHandler { private final Decoder decoder; - private SynchronousMethodHandler(Target target, Client client, Provider retryer, Wire wire, MethodMetadata metadata, - BuildTemplateFromArgs buildTemplateFromArgs, Options options, Decoder decoder, ErrorDecoder errorDecoder) { - super(target, client, retryer, wire, metadata, buildTemplateFromArgs, options, errorDecoder); + private SynchronousMethodHandler(Target target, Client client, Provider retryer, Logger logger, + Logger.Level logLevel, MethodMetadata metadata, + BuildTemplateFromArgs buildTemplateFromArgs, Options options, Decoder decoder, + ErrorDecoder errorDecoder) { + super(target, client, retryer, logger, logLevel, metadata, buildTemplateFromArgs, options, errorDecoder); this.decoder = checkNotNull(decoder, "decoder for %s", target); } @@ -79,18 +85,21 @@ private SynchronousMethodHandler(Target target, Client client, Provider target; protected final Client client; protected final Provider retryer; - protected final Wire wire; + protected final Logger logger; + protected final Logger.Level logLevel; protected final BuildTemplateFromArgs buildTemplateFromArgs; protected final Options options; protected final ErrorDecoder errorDecoder; - private MethodHandler(Target target, Client client, Provider retryer, Wire wire, MethodMetadata metadata, - BuildTemplateFromArgs buildTemplateFromArgs, Options options, ErrorDecoder errorDecoder) { + private MethodHandler(Target target, Client client, Provider retryer, Logger logger, + 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.wire = checkNotNull(wire, "wire for %s", target); + this.logger = checkNotNull(logger, "logger for %s", target); + this.logLevel = checkNotNull(logLevel, "logLevel for %s", target); this.metadata = checkNotNull(metadata, "metadata for %s", target); this.buildTemplateFromArgs = checkNotNull(buildTemplateFromArgs, "metadata for %s", target); this.options = checkNotNull(options, "options for %s", target); @@ -114,15 +123,24 @@ public Object executeAndDecode(Object[] argv, RequestTemplate template) throws Throwable { // create the request from a mutable copy of the input template. Request request = target.apply(new RequestTemplate(template)); - wire.wireRequest(target, request); + + if (logLevel.ordinal() > Logger.Level.NONE.ordinal()) { + logger.logRequest(target, logLevel, request); + } + Response response; + long start = System.nanoTime(); try { response = client.execute(request, options); } catch (IOException e) { throw errorExecuting(request, e); } + long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); + try { - response = wire.wireAndRebufferResponse(target, response); + if (logLevel.ordinal() > Logger.Level.NONE.ordinal()) { + response = logger.logAndRebufferResponse(target, logLevel, response, elapsedTime); + } if (response.status() >= 200 && response.status() < 300) { return decode(argv, response); } else { diff --git a/feign-core/src/main/java/feign/Wire.java b/feign-core/src/main/java/feign/Wire.java deleted file mode 100644 index fda8fae75f..0000000000 --- a/feign-core/src/main/java/feign/Wire.java +++ /dev/null @@ -1,147 +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.BufferedReader; -import java.io.IOException; -import java.io.Reader; -import java.text.SimpleDateFormat; -import java.util.logging.FileHandler; -import java.util.logging.Level; -import java.util.logging.LogRecord; -import java.util.logging.Logger; -import java.util.logging.SimpleFormatter; - -import static feign.Util.ensureClosed; -import static feign.Util.valuesOrEmpty; - -/* Writes http headers and body. Plumb to your favorite log impl. */ -public abstract class Wire { - /* logs to the category {@link Wire} at {@link Level#FINE}. */ - public static class ErrorWire extends Wire { - final Logger logger = Logger.getLogger(Wire.class.getName()); - - @Override protected void log(Target target, String format, Object... args) { - System.err.printf(format + "%n", args); - } - } - - /* logs to the category {@link Wire} at {@link Level#FINE}, if loggable. */ - public static class LoggingWire extends Wire { - final Logger logger = Logger.getLogger(Wire.class.getName()); - - @Override void wireRequest(Target target, Request request) { - if (logger.isLoggable(Level.FINE)) { - super.wireRequest(target, request); - } - } - - @Override Response wireAndRebufferResponse(Target target, Response response) throws IOException { - if (logger.isLoggable(Level.FINE)) { - return super.wireAndRebufferResponse(target, response); - } - return response; - } - - @Override protected void log(Target target, String format, Object... args) { - logger.fine(String.format(format, args)); - } - - /* helper that configures jul to sanely log messages. */ - public LoggingWire appendToFile(String logfile) { - final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); - logger.setLevel(Level.FINE); - try { - FileHandler handler = new FileHandler(logfile, true); - handler.setFormatter(new SimpleFormatter() { - @Override - public String format(LogRecord record) { - String timestamp = sdf.format(new java.util.Date(record.getMillis())); // NOPMD - return String.format("%s %s%n", timestamp, record.getMessage()); // NOPMD - } - }); - logger.addHandler(handler); - } catch (IOException e) { - throw new IllegalStateException("Could not add file handler.", e); - } - return this; - } - } - - public static class NoOpWire extends Wire { - @Override void wireRequest(Target target, Request request) { - } - - @Override Response wireAndRebufferResponse(Target target, Response response) throws IOException { - return response; - } - - @Override - protected void log(Target target, String format, Object... args) { - } - } - - /** - * 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} - */ - protected abstract void log(Target target, String format, Object... args); - - void wireRequest(Target target, Request request) { - log(target, ">> %s %s HTTP/1.1", request.method(), request.url()); - for (String field : request.headers().keySet()) { - for (String value : valuesOrEmpty(request.headers(), field)) { - log(target, ">> %s: %s", field, value); - } - } - - if (request.body() != null) { - log(target, ">> "); // CRLF - log(target, ">> %s", request.body()); - } - } - - Response wireAndRebufferResponse(Target target, Response response) throws IOException { - log(target, "<< HTTP/1.1 %s %s", response.status(), response.reason()); - for (String field : response.headers().keySet()) { - for (String value : valuesOrEmpty(response.headers(), field)) { - log(target, "<< %s: %s", field, value); - } - } - - if (response.body() != null) { - log(target, "<< "); // CRLF - Reader body = response.body().asReader(); - try { - StringBuilder buffered = new StringBuilder(); - BufferedReader reader = new BufferedReader(body); - String line; - while ((line = reader.readLine()) != null) { - buffered.append(line); - log(target, "<< %s", line); - } - return Response.create(response.status(), response.reason(), response.headers(), buffered.toString()); - } finally { - ensureClosed(response.body()); - } - } - return response; - } -} diff --git a/feign-core/src/test/java/feign/LoggerTest.java b/feign-core/src/test/java/feign/LoggerTest.java new file mode 100644 index 0000000000..22ccc041b3 --- /dev/null +++ b/feign-core/src/test/java/feign/LoggerTest.java @@ -0,0 +1,137 @@ +/* + * 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.collect.ImmutableMap; +import com.google.mockwebserver.MockResponse; +import com.google.mockwebserver.MockWebServer; +import dagger.Provides; +import feign.codec.Decoder; +import feign.codec.StringDecoder; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import javax.inject.Named; +import javax.inject.Singleton; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +@Test +public class LoggerTest { + + Logger logger = new Logger() { + @Override protected void log(Target target, String format, Object... args) { + messages.add(String.format(format, args)); + } + }; + + List messages = new ArrayList(); + + @BeforeMethod void clear() { + messages.clear(); + } + + interface SendsStuff { + + @RequestLine("POST /") + @Headers("Content-Type: application/json") + @Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D") + String login( + @Named("customer_name") String customer, + @Named("user_name") String user, @Named("password") String password); + } + + @DataProvider(name = "levelToOutput") + public Object[][] createData() { + Object[][] data = new Object[4][2]; + data[0][0] = Logger.Level.NONE; + data[0][1] = Arrays.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\\)" + ); + 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\\)" + ); + 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\\)" + ); + return data; + } + + @Test(dataProvider = "levelToOutput") + public void levelEmits(final Logger.Level logLevel, List expectedMessages) throws IOException, InterruptedException { + final MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse().setResponseCode(200).setBody("foo")); + server.play(); + + @dagger.Module(overrides = true, library = true) class Module { + @Provides @Singleton Map decoders() { + return ImmutableMap.of("SendsStuff", new StringDecoder()); + } + + @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()); + + 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)); + } + + assertEquals(new String(server.takeRequest().getBody()), + "{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}"); + } finally { + server.shutdown(); + } + } +} From 20bff15fdcda4ad4432a1ed7095780ada1e8c238 Mon Sep 17 00:00:00 2001 From: adriancole Date: Sun, 14 Jul 2013 16:11:13 -0700 Subject: [PATCH 066/179] Normalized to Decoder.TextStream and Encoder.Text; replaced Map bindings with Set --- CHANGES.md | 12 +- README.md | 51 ++- feign-core/src/main/java/feign/Contract.java | 3 +- feign-core/src/main/java/feign/Feign.java | 46 +- .../src/main/java/feign/FeignException.java | 8 +- .../src/main/java/feign/MethodHandler.java | 34 +- .../src/main/java/feign/MethodMetadata.java | 10 + .../src/main/java/feign/ReflectiveFeign.java | 161 ++++--- .../src/main/java/feign/RequestTemplate.java | 2 +- feign-core/src/main/java/feign/Types.java | 414 ++++++++++++++++++ feign-core/src/main/java/feign/Util.java | 29 ++ .../main/java/feign/codec/BodyEncoder.java | 55 --- .../java/feign/codec/DecodeException.java | 45 ++ .../src/main/java/feign/codec/Decoder.java | 97 ++-- .../src/main/java/feign/codec/Decoders.java | 184 +++++--- .../java/feign/codec/EncodeException.java | 45 ++ .../src/main/java/feign/codec/Encoder.java | 75 ++++ .../main/java/feign/codec/ErrorDecoder.java | 4 +- .../main/java/feign/codec/FormEncoder.java | 40 -- .../src/main/java/feign/codec/SAXDecoder.java | 60 +-- .../main/java/feign/codec/StringDecoder.java | 22 +- .../test/java/feign/DefaultContractTest.java | 32 +- feign-core/src/test/java/feign/FeignTest.java | 168 +++++-- .../src/test/java/feign/LoggerTest.java | 10 +- feign-core/src/test/java/feign/UtilTest.java | 92 ++++ .../feign/codec/DefaultErrorDecoderTest.java | 8 +- .../java/feign/examples/GitHubExample.java | 63 ++- .../test/java/feign/examples/IAMExample.java | 12 +- .../java/feign/jaxrs/JAXRSContractTest.java | 25 ++ .../feign/jaxrs/examples/GitHubExample.java | 48 +- .../java/feign/jaxrs/examples/IAMExample.java | 11 +- 31 files changed, 1355 insertions(+), 511 deletions(-) create mode 100644 feign-core/src/main/java/feign/Types.java delete mode 100644 feign-core/src/main/java/feign/codec/BodyEncoder.java create mode 100644 feign-core/src/main/java/feign/codec/DecodeException.java create mode 100644 feign-core/src/main/java/feign/codec/EncodeException.java create mode 100644 feign-core/src/main/java/feign/codec/Encoder.java delete mode 100644 feign-core/src/main/java/feign/codec/FormEncoder.java create mode 100644 feign-core/src/test/java/feign/UtilTest.java diff --git a/CHANGES.md b/CHANGES.md index 685f9893a0..bf2eb4e20d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,8 +1,14 @@ ### Version 3.0 * Wire is now Logger, with configurable Logger.Level. -* decoupled ErrorDecoder from fallback handling -* Decoders can throw checked exceptions, but needn't declare Throwable -* Decoders no longer read methodKey +* changed codec to be similar to [WebSocket JSR 356](http://docs.oracle.com/javaee/7/api/javax/websocket/package-summary.html) + * Decoder is now `Decoder.TextStream` + * BodyEncoder is now `Encoder.Text` + * FormEncoder is now `Encoder.Text>` +* Encoder and Decoders are specified via `Provides.Type.SET` binding. +* Default Encoder and Form Encoder is `Encoder.Text` +* Default Decoder is `Decoder.TextStream` +* ErrorDecoder now returns Exception, not fallback. +* There can only be one `ErrorDecoder` and `Request.Options` binding now. ### Version 2.0.0 * removes guava and jax-rs dependencies diff --git a/README.md b/README.md index ea1622eba2..b60ba16e4c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Feign makes writing java http clients easier -Feign is a java to http client binder inspired by [Dagger](https://github.com/square/dagger), [Retrofit](https://github.com/square/retrofit), [jclouds](https://github.com/jclouds/jclouds), and [JAXRS-2.0](https://jax-rs-spec.java.net/nonav/2.0/apidocs/index.html). Feign's first goal was reducing the complexity of binding [Denominator](https://github.com/Netflix/Denominator) uniformly to http apis regardless of [restfulness](http://www.slideshare.net/adrianfcole/99problems). +Feign is a java to http client binder inspired by [Dagger](https://github.com/square/dagger), [Retrofit](https://github.com/square/retrofit), [JAXRS-2.0](https://jax-rs-spec.java.net/nonav/2.0/apidocs/index.html), and [WebSockets](http://www.oracle.com/technetwork/articles/java/jsr356-1937161.html). Feign's first goal was reducing the complexity of binding [Denominator](https://github.com/Netflix/Denominator) uniformly to http apis regardless of [restfulness](http://www.slideshare.net/adrianfcole/99problems). ### Why Feign and not X? @@ -35,26 +35,39 @@ public static void main(String... args) { } ``` ### Decoders -The last argument to `Feign.create` specifies how to decode the responses. You can plug-in your favorite library, such as gson, or use builtin RegEx Pattern decoders. Here's how the Gson module looks. +The last argument to `Feign.create` specifies how to decode the responses, modeled in Dagger. Here's how it looks to wire in a default gson decoder: ```java @Module(overrides = true, library = true) static class GsonModule { - @Provides @Singleton Map decoders() { - return ImmutableMap.of("GitHub", gsonDecoder); + @Provides(type = SET) Decoder decoder() { + return new Decoder.TextStream() { + Gson gson = new Gson(); + + @Override public Object decode(Reader reader, Type type) throws IOException { + try { + return gson.fromJson(reader, type); + } catch (JsonIOException e) { + if (e.getCause() != null && e.getCause() instanceof IOException) { + throw IOException.class.cast(e.getCause()); + } + throw e; + } + } + }; } - - final Decoder gsonDecoder = new Decoder() { - Gson gson = new Gson(); - - @Override public Object decode(String methodKey, Reader reader, Type type) { - return gson.fromJson(reader, type); - } - }; } ``` Feign doesn't offer a built-in json decoder as you can see above it is very few lines of code to wire yours in. If you are a jackson user, you'd probably thank us for not dragging in a dependency you don't use. +#### Type-specific Decoders +The generic parameter of `Decoder.TextStream` designates which The type parameter is either a concrete type, or `Object`, if your decoder can handle multiple types. To add a type-specific decoder, ensure your type parameter is correct. Here's an example of an xml decoder that will only apply to methods that return `ZoneList`. + +``` +@Provides(type = SET) Decoder zoneListDecoder(Provider handlers) { + return new SAXDecoder(handlers){}; +} +``` ### Multiple Interfaces Feign can produce multiple api interfaces. These are defined as `Target` (default `HardCodedTarget`), which allow for dynamic discovery and decoration of requests prior to execution. @@ -89,10 +102,14 @@ MyService api = Feign.create(MyService.class, "https://myAppProd", new RibbonMod #### Dagger Feign can be directly wired into Dagger which keeps things at compile time and Android friendly. As opposed to exposing builders for config, Feign intends users to embed their config in Dagger. -Almost all configuration of Feign is represented as Map bindings, where the key is either the simple name (ex. `GitHub`) or the method (ex. `GitHub#contributors()`) in javadoc link format. For example, the following routes all decoding to gson: +Where possible, Feign configuration uses normal Dagger conventions. For example, `Decoder` bindings are of `Provider.Type.SET`, meaning you can make multiple bindings for all the different types you return. Here's an example of multiple decoder bindings. ```java -@Provides @Singleton Map decoders() { - return ImmutableMap.of("GitHub", gsonDecoder); +@Provides(type = SET) Decoder recordListDecoder(Provider handlers) { + return new SAXDecoder>(handlers){}; +} + +@Provides(type = SET) Decoder directionalRecordListDecoder(Provider handlers) { + return new SAXDecoder>(handlers){}; } ``` #### Logging @@ -117,8 +134,8 @@ Here's how our IAM example grabs only one xml element from a response. ```java @Module(overrides = true, library = true) static class IAMModule { - @Provides @Singleton Map decoders() { - return ImmutableMap.of("IAM#arn()", Decoders.firstGroup("([\\S&&[^<]]+)")); + @Provides(type = SET) Decoder arnDecoder() { + return Decoders.firstGroup("([\\S&&[^<]]+)"); } } ``` diff --git a/feign-core/src/main/java/feign/Contract.java b/feign-core/src/main/java/feign/Contract.java index 407bedce34..7669fb9549 100644 --- a/feign-core/src/main/java/feign/Contract.java +++ b/feign-core/src/main/java/feign/Contract.java @@ -73,6 +73,7 @@ public MethodMetadata parseAndValidatateMetadata(Method method) { checkState(data.formParams().isEmpty(), "Body parameters cannot be used with @FormParam parameters."); checkState(data.bodyIndex() == null, "Method has too many Body parameters: %s", method); data.bodyIndex(i); + data.bodyType(method.getGenericParameterTypes()[i]); } } return data; @@ -112,7 +113,7 @@ protected void nameParam(MethodMetadata data, String name, int i) { data.indexToName().put(i, names); } - static class DefaultContract extends Contract { + static class Default extends Contract { @Override protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) { diff --git a/feign-core/src/main/java/feign/Feign.java b/feign-core/src/main/java/feign/Feign.java index 867d24f79c..b5de31cf68 100644 --- a/feign-core/src/main/java/feign/Feign.java +++ b/feign-core/src/main/java/feign/Feign.java @@ -15,23 +15,21 @@ */ package feign; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import javax.net.ssl.SSLSocketFactory; - import dagger.ObjectGraph; import dagger.Provides; +import feign.Logger.NoOpLogger; import feign.Request.Options; import feign.Target.HardCodedTarget; -import feign.Logger.NoOpLogger; -import feign.codec.BodyEncoder; import feign.codec.Decoder; +import feign.codec.Encoder; import feign.codec.ErrorDecoder; -import feign.codec.FormEncoder; + +import javax.net.ssl.SSLSocketFactory; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; /** * Feign's purpose is to ease development against http apis that feign @@ -78,16 +76,16 @@ public static ObjectGraph createObjectGraph(Object... modules) { return ObjectGraph.create(modulesForGraph(modules).toArray()); } + @SuppressWarnings("rawtypes") @dagger.Module(complete = false, injects = Feign.class, library = true) public static class Defaults { - @Provides - Logger.Level logLevel() { + @Provides Logger.Level logLevel() { return Logger.Level.NONE; } @Provides Contract contract() { - return new Contract.DefaultContract(); + return new Contract.Default(); } @Provides SSLSocketFactory sslSocketFactory() { @@ -106,24 +104,20 @@ Logger.Level logLevel() { return new NoOpLogger(); } - @Provides Map noOptions() { - return Collections.emptyMap(); - } - - @Provides Map noBodyEncoders() { - return Collections.emptyMap(); + @Provides ErrorDecoder errorDecoder() { + return new ErrorDecoder.Default(); } - @Provides Map noFormEncoders() { - return Collections.emptyMap(); + @Provides Options options() { + return new Options(); } - @Provides Map noDecoders() { - return Collections.emptyMap(); + @Provides Set noEncoders() { + return Collections.emptySet(); } - @Provides Map noErrorDecoders() { - return Collections.emptyMap(); + @Provides Set noDecoders() { + return Collections.emptySet(); } } diff --git a/feign-core/src/main/java/feign/FeignException.java b/feign-core/src/main/java/feign/FeignException.java index 7ceef0d13e..ebdf7b0650 100644 --- a/feign-core/src/main/java/feign/FeignException.java +++ b/feign-core/src/main/java/feign/FeignException.java @@ -15,12 +15,12 @@ */ package feign; +import static java.lang.String.format; + import java.io.IOException; import feign.codec.StringDecoder; -import static java.lang.String.format; - /** * Origin exception type for all Http Apis. */ @@ -34,8 +34,8 @@ static FeignException errorReading(Request request, Response response, IOExcepti public static FeignException errorStatus(String methodKey, Response response) { String message = format("status %s reading %s", response.status(), methodKey); try { - Object body = toString.decode(response, String.class); - if (body != null) { + if (response.body() != null) { + String body = toString.decode(response.body().asReader(), String.class); response = Response.create(response.status(), response.reason(), response.headers(), body.toString()); message += "; content:\n" + body; } diff --git a/feign-core/src/main/java/feign/MethodHandler.java b/feign-core/src/main/java/feign/MethodHandler.java index f58bf3a5de..0cef816e9d 100644 --- a/feign-core/src/main/java/feign/MethodHandler.java +++ b/feign-core/src/main/java/feign/MethodHandler.java @@ -15,16 +15,16 @@ */ package feign; -import java.io.IOException; -import java.util.concurrent.TimeUnit; - -import javax.inject.Inject; -import javax.inject.Provider; - import feign.Request.Options; +import feign.codec.DecodeException; import feign.codec.Decoder; import feign.codec.ErrorDecoder; +import javax.inject.Inject; +import javax.inject.Provider; +import java.io.IOException; +import java.util.concurrent.TimeUnit; + import static feign.FeignException.errorExecuting; import static feign.FeignException.errorReading; import static feign.Util.checkNotNull; @@ -54,19 +54,19 @@ static class Factory { } public MethodHandler create(Target target, MethodMetadata md, BuildTemplateFromArgs buildTemplateFromArgs, - Options options, Decoder decoder, ErrorDecoder errorDecoder) { - return new SynchronousMethodHandler(target, client, retryer, logger, logLevel, md, buildTemplateFromArgs, - options, decoder, errorDecoder); + Options options, Decoder.TextStream decoder, ErrorDecoder errorDecoder) { + return new SynchronousMethodHandler(target, client, retryer, logger, logLevel, md, buildTemplateFromArgs, options, + decoder, errorDecoder); } } static final class SynchronousMethodHandler extends MethodHandler { - private final Decoder decoder; + private final Decoder.TextStream decoder; private SynchronousMethodHandler(Target target, Client client, Provider retryer, Logger logger, Logger.Level logLevel, MethodMetadata metadata, - BuildTemplateFromArgs buildTemplateFromArgs, Options options, Decoder decoder, - ErrorDecoder errorDecoder) { + BuildTemplateFromArgs buildTemplateFromArgs, Options options, + Decoder.TextStream decoder, ErrorDecoder errorDecoder) { super(target, client, retryer, logger, logLevel, metadata, buildTemplateFromArgs, options, errorDecoder); this.decoder = checkNotNull(decoder, "decoder for %s", target); } @@ -74,10 +74,16 @@ private SynchronousMethodHandler(Target target, Client client, Provider formParams = new ArrayList(); private Map> indexToName = new LinkedHashMap>(); @@ -74,6 +75,15 @@ MethodMetadata bodyIndex(Integer bodyIndex) { return this; } + public Type bodyType() { + return bodyType; + } + + MethodMetadata bodyType(Type bodyType) { + this.bodyType = bodyType; + return this; + } + public RequestTemplate template() { return template; } diff --git a/feign-core/src/main/java/feign/ReflectiveFeign.java b/feign-core/src/main/java/feign/ReflectiveFeign.java index bc77f5e8b3..50289e16e8 100644 --- a/feign-core/src/main/java/feign/ReflectiveFeign.java +++ b/feign-core/src/main/java/feign/ReflectiveFeign.java @@ -15,28 +15,31 @@ */ package feign; +import dagger.Provides; +import feign.Request.Options; +import feign.codec.Decoder; +import feign.codec.EncodeException; +import feign.codec.Encoder; +import feign.codec.ErrorDecoder; +import feign.codec.StringDecoder; + +import javax.inject.Inject; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; +import java.lang.reflect.Type; import java.util.Collection; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; - -import javax.inject.Inject; - -import dagger.Provides; -import feign.MethodHandler.Factory; -import feign.Request.Options; -import feign.codec.BodyEncoder; -import feign.codec.Decoder; -import feign.codec.ErrorDecoder; -import feign.codec.FormEncoder; -import feign.codec.StringDecoder; +import java.util.Set; import static feign.Util.checkArgument; import static feign.Util.checkNotNull; +import static feign.Util.checkState; +import static feign.Util.resolveLastTypeParameter; import static java.lang.String.format; @SuppressWarnings("rawtypes") @@ -96,9 +99,7 @@ static class FeignInvocationHandler implements InvocationHandler { } } - @dagger.Module(complete = false,// Config - injects = Feign.class, library = true// provides Feign - ) + @dagger.Module(complete = false, injects = Feign.class, library = true) public static class Module { @Provides Feign provideFeign(ReflectiveFeign in) { @@ -113,63 +114,82 @@ private static IllegalStateException noConfig(String configKey, Class type) { static final class ParseHandlersByName { private final Contract contract; - private final Map options; - private final Map bodyEncoders; - private final Map formEncoders; - private final Map decoders; - private final Map errorDecoders; - private final Factory factory; - - @Inject ParseHandlersByName(Contract contract, Map options, Map bodyEncoders, - Map formEncoders, Map decoders, - Map errorDecoders, Factory factory) { + private final Options options; + private final Map> encoders = new HashMap>(); + private final Encoder.Text> formEncoder; + private final Map> decoders = new HashMap>(); + private final ErrorDecoder errorDecoder; + private final MethodHandler.Factory factory; + + @SuppressWarnings("unchecked") + @Inject ParseHandlersByName(Contract contract, Options options, Set encoders, Set decoders, + ErrorDecoder errorDecoder, MethodHandler.Factory factory) { this.contract = contract; this.options = options; - this.bodyEncoders = bodyEncoders; - this.formEncoders = formEncoders; - this.decoders = decoders; this.factory = factory; - this.errorDecoders = errorDecoders; + this.errorDecoder = errorDecoder; + for (Encoder encoder : encoders) { + checkState(encoder instanceof Encoder.Text, + "Currently, only Encoder.Text is supported. Found: ", encoder); + Type type = resolveLastTypeParameter(encoder.getClass(), Encoder.class); + this.encoders.put(type, Encoder.Text.class.cast(encoder)); + } + try { + Type formEncoderType = getClass().getDeclaredField("formEncoder").getGenericType(); + Type formType = resolveLastTypeParameter(formEncoderType, Encoder.class); + Encoder.Text formEncoder = this.encoders.get(formType); + if (formEncoder == null) { + formEncoder = this.encoders.get(Object.class); + } + this.formEncoder = (Encoder.Text) formEncoder; + } catch (NoSuchFieldException e) { + throw new AssertionError(e); + } + StringDecoder stringDecoder = new StringDecoder(); + this.decoders.put(void.class, stringDecoder); + this.decoders.put(Response.class, stringDecoder); + this.decoders.put(String.class, stringDecoder); + for (Decoder decoder : decoders) { + checkState(decoder instanceof Decoder.TextStream, + "Currently, only Decoder.TextStream is supported. Found: ", decoder); + Type type = resolveLastTypeParameter(decoder.getClass(), Decoder.class); + this.decoders.put(type, Decoder.TextStream.class.cast(decoder)); + } } public Map apply(Target key) { List metadata = contract.parseAndValidatateMetadata(key.type()); Map result = new LinkedHashMap(); for (MethodMetadata md : metadata) { - Options options = forMethodOrClass(this.options, md.configKey()); - if (options == null) { - options = new Options(); - } - Decoder decoder = forMethodOrClass(decoders, md.configKey()); - if (decoder == null - && (md.returnType() == void.class || md.returnType() == Response.class)) { - decoder = new StringDecoder(); - } + Decoder.TextStream decoder = decoders.get(md.returnType()); if (decoder == null) { - throw noConfig(md.configKey(), Decoder.class); + decoder = decoders.get(Object.class); } - ErrorDecoder errorDecoder = forMethodOrClass(errorDecoders, md.configKey()); - if (errorDecoder == null) { - errorDecoder = ErrorDecoder.DEFAULT; + if (decoder == null) { + throw new IllegalStateException(format("%s needs @Provides(type = Set) Decoder decoder()" + + "{ // Decoder.TextStream<%s> or Decoder.TextStream}", md.configKey(), md.returnType())); } BuildTemplateByResolvingArgs buildTemplate; if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) { - FormEncoder formEncoder = forMethodOrClass(formEncoders, md.configKey()); if (formEncoder == null) { - throw noConfig(md.configKey(), FormEncoder.class); + throw new IllegalStateException(format("%s needs @Provides(type = Set) Encoder encoder()" + + "{ // Encoder.Text> or Encoder.Text}", md.configKey())); } buildTemplate = new BuildFormEncodedTemplateFromArgs(md, formEncoder); } else if (md.bodyIndex() != null) { - BodyEncoder bodyEncoder = forMethodOrClass(bodyEncoders, md.configKey()); - if (bodyEncoder == null) { - throw noConfig(md.configKey(), BodyEncoder.class); + Encoder.Text encoder = encoders.get(md.bodyType()); + if (encoder == null) { + encoder = encoders.get(Object.class); + } + if (encoder == null) { + throw new IllegalStateException(format("%s needs @Provides(type = Set) Encoder encoder()" + + "{ // Encoder.Text<%s> or Encoder.Text}", md.bodyType(), md.returnType())); } - buildTemplate = new BuildBodyEncodedTemplateFromArgs(md, bodyEncoder); + buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder); } else { buildTemplate = new BuildTemplateByResolvingArgs(md); } - result.put(md.configKey(), - factory.create(key, md, buildTemplate, options, decoder, errorDecoder)); + result.put(md.configKey(), factory.create(key, md, buildTemplate, options, decoder, errorDecoder)); } return result; } @@ -206,9 +226,9 @@ protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map> formEncoder; - private BuildFormEncodedTemplateFromArgs(MethodMetadata metadata, FormEncoder formEncoder) { + private BuildFormEncodedTemplateFromArgs(MethodMetadata metadata, Encoder.Text> formEncoder) { super(metadata); this.formEncoder = formEncoder; } @@ -220,40 +240,37 @@ protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map encoder; - private BuildBodyEncodedTemplateFromArgs(MethodMetadata metadata, BodyEncoder bodyEncoder) { + private BuildEncodedTemplateFromArgs(MethodMetadata metadata, Encoder.Text encoder) { super(metadata); - this.bodyEncoder = bodyEncoder; + this.encoder = encoder; } @Override protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map variables) { Object body = argv[metadata.bodyIndex()]; checkArgument(body != null, "Body parameter %s was null", metadata.bodyIndex()); - bodyEncoder.encodeBody(body, mutable); + try { + mutable.body(encoder.encode(body)); + } catch (EncodeException e) { + throw e; + } catch (RuntimeException e) { + throw new EncodeException(e.getMessage(), e); + } return super.resolve(argv, mutable, variables); } } - - static T forMethodOrClass(Map config, String configKey) { - if (config.containsKey(configKey)) { - return config.get(configKey); - } - String classKey = toClassKey(configKey); - if (config.containsKey(classKey)) { - return config.get(classKey); - } - return null; - } - - public static String toClassKey(String methodKey) { - return methodKey.substring(0, methodKey.indexOf('#')); - } } diff --git a/feign-core/src/main/java/feign/RequestTemplate.java b/feign-core/src/main/java/feign/RequestTemplate.java index 5e49c45e43..5b2b9a46e6 100644 --- a/feign-core/src/main/java/feign/RequestTemplate.java +++ b/feign-core/src/main/java/feign/RequestTemplate.java @@ -400,7 +400,7 @@ public Map> headers() { /** * replaces the {@link feign.Util#CONTENT_LENGTH} header. *
- * Usually populated by {@link feign.codec.BodyEncoder} or {@link feign.codec.FormEncoder} + * Usually populated by an {@link feign.codec.Encoder}. * * @see Request#body() */ diff --git a/feign-core/src/main/java/feign/Types.java b/feign-core/src/main/java/feign/Types.java new file mode 100644 index 0000000000..bfdc00fd54 --- /dev/null +++ b/feign-core/src/main/java/feign/Types.java @@ -0,0 +1,414 @@ +/* + * Copyright (C) 2008 Google 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.lang.reflect.Array; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.GenericDeclaration; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.Arrays; +import java.util.NoSuchElementException; + +/** + * Static methods for working with types. + * + * @author Bob Lee + * @author Jesse Wilson + */ +final class Types { + private static final Type[] EMPTY_TYPE_ARRAY = new Type[0]; + + private Types() { + // No instances. + } + + static Class getRawType(Type type) { + if (type instanceof Class) { + // Type is a normal class. + return (Class) type; + + } else if (type instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) type; + + // I'm not exactly sure why getRawType() returns Type instead of Class. Neal isn't either but + // suspects some pathological case related to nested classes exists. + Type rawType = parameterizedType.getRawType(); + if (!(rawType instanceof Class)) throw new IllegalArgumentException(); + return (Class) rawType; + + } else if (type instanceof GenericArrayType) { + Type componentType = ((GenericArrayType) type).getGenericComponentType(); + return Array.newInstance(getRawType(componentType), 0).getClass(); + + } else if (type instanceof TypeVariable) { + // We could use the variable's bounds, but that won't work if there are multiple. Having a raw + // type that's more general than necessary is okay. + return Object.class; + + } else if (type instanceof WildcardType) { + return getRawType(((WildcardType) type).getUpperBounds()[0]); + + } else { + String className = type == null ? "null" : type.getClass().getName(); + throw new IllegalArgumentException("Expected a Class, ParameterizedType, or " + + "GenericArrayType, but <" + type + "> is of type " + className); + } + } + + /** Returns true if {@code a} and {@code b} are equal. */ + static boolean equals(Type a, Type b) { + if (a == b) { + return true; // Also handles (a == null && b == null). + + } else if (a instanceof Class) { + return a.equals(b); // Class already specifies equals(). + + } else if (a instanceof ParameterizedType) { + if (!(b instanceof ParameterizedType)) return false; + ParameterizedType pa = (ParameterizedType) a; + ParameterizedType pb = (ParameterizedType) b; + return equal(pa.getOwnerType(), pb.getOwnerType()) + && pa.getRawType().equals(pb.getRawType()) + && Arrays.equals(pa.getActualTypeArguments(), pb.getActualTypeArguments()); + + } else if (a instanceof GenericArrayType) { + if (!(b instanceof GenericArrayType)) return false; + GenericArrayType ga = (GenericArrayType) a; + GenericArrayType gb = (GenericArrayType) b; + return equals(ga.getGenericComponentType(), gb.getGenericComponentType()); + + } else if (a instanceof WildcardType) { + if (!(b instanceof WildcardType)) return false; + WildcardType wa = (WildcardType) a; + WildcardType wb = (WildcardType) b; + return Arrays.equals(wa.getUpperBounds(), wb.getUpperBounds()) + && Arrays.equals(wa.getLowerBounds(), wb.getLowerBounds()); + + } else if (a instanceof TypeVariable) { + if (!(b instanceof TypeVariable)) return false; + TypeVariable va = (TypeVariable) a; + TypeVariable vb = (TypeVariable) b; + return va.getGenericDeclaration() == vb.getGenericDeclaration() + && va.getName().equals(vb.getName()); + + } else { + return false; // This isn't a type we support! + } + } + + /** + * Returns the generic supertype for {@code supertype}. For example, given a class {@code + * IntegerSet}, the result for when supertype is {@code Set.class} is {@code Set} and the + * result when the supertype is {@code Collection.class} is {@code Collection}. + */ + static Type getGenericSupertype(Type context, Class rawType, Class toResolve) { + if (toResolve == rawType) return context; + + // We skip searching through interfaces if unknown is an interface. + if (toResolve.isInterface()) { + Class[] interfaces = rawType.getInterfaces(); + for (int i = 0, length = interfaces.length; i < length; i++) { + if (interfaces[i] == toResolve) { + return rawType.getGenericInterfaces()[i]; + } else if (toResolve.isAssignableFrom(interfaces[i])) { + return getGenericSupertype(rawType.getGenericInterfaces()[i], interfaces[i], toResolve); + } + } + } + + // Check our supertypes. + if (!rawType.isInterface()) { + while (rawType != Object.class) { + Class rawSupertype = rawType.getSuperclass(); + if (rawSupertype == toResolve) { + return rawType.getGenericSuperclass(); + } else if (toResolve.isAssignableFrom(rawSupertype)) { + return getGenericSupertype(rawType.getGenericSuperclass(), rawSupertype, toResolve); + } + rawType = rawSupertype; + } + } + + // We can't resolve this further. + return toResolve; + } + + private static int indexOf(Object[] array, Object toFind) { + for (int i = 0; i < array.length; i++) { + if (toFind.equals(array[i])) return i; + } + throw new NoSuchElementException(); + } + + private static boolean equal(Object a, Object b) { + return a == b || (a != null && a.equals(b)); + } + + private static int hashCodeOrZero(Object o) { + return o != null ? o.hashCode() : 0; + } + + static String typeToString(Type type) { + return type instanceof Class ? ((Class) type).getName() : type.toString(); + } + + /** + * Returns the generic form of {@code supertype}. For example, if this is {@code + * ArrayList}, this returns {@code Iterable} given the input {@code + * Iterable.class}. + * + * @param supertype a superclass of, or interface implemented by, this. + */ + static Type getSupertype(Type context, Class contextRawType, Class supertype) { + if (!supertype.isAssignableFrom(contextRawType)) throw new IllegalArgumentException(); + return resolve(context, contextRawType, + getGenericSupertype(context, contextRawType, supertype)); + } + + static Type resolve(Type context, Class contextRawType, Type toResolve) { + // This implementation is made a little more complicated in an attempt to avoid object-creation. + while (true) { + if (toResolve instanceof TypeVariable) { + TypeVariable typeVariable = (TypeVariable) toResolve; + toResolve = resolveTypeVariable(context, contextRawType, typeVariable); + if (toResolve == typeVariable) { + return toResolve; + } + + } else if (toResolve instanceof Class && ((Class) toResolve).isArray()) { + Class original = (Class) toResolve; + Type componentType = original.getComponentType(); + Type newComponentType = resolve(context, contextRawType, componentType); + return componentType == newComponentType ? original : new GenericArrayTypeImpl( + newComponentType); + + } else if (toResolve instanceof GenericArrayType) { + GenericArrayType original = (GenericArrayType) toResolve; + Type componentType = original.getGenericComponentType(); + Type newComponentType = resolve(context, contextRawType, componentType); + return componentType == newComponentType ? original : new GenericArrayTypeImpl( + newComponentType); + + } else if (toResolve instanceof ParameterizedType) { + ParameterizedType original = (ParameterizedType) toResolve; + Type ownerType = original.getOwnerType(); + Type newOwnerType = resolve(context, contextRawType, ownerType); + boolean changed = newOwnerType != ownerType; + + Type[] args = original.getActualTypeArguments(); + for (int t = 0, length = args.length; t < length; t++) { + Type resolvedTypeArgument = resolve(context, contextRawType, args[t]); + if (resolvedTypeArgument != args[t]) { + if (!changed) { + args = args.clone(); + changed = true; + } + args[t] = resolvedTypeArgument; + } + } + + return changed + ? new ParameterizedTypeImpl(newOwnerType, original.getRawType(), args) + : original; + + } else if (toResolve instanceof WildcardType) { + WildcardType original = (WildcardType) toResolve; + Type[] originalLowerBound = original.getLowerBounds(); + Type[] originalUpperBound = original.getUpperBounds(); + + if (originalLowerBound.length == 1) { + Type lowerBound = resolve(context, contextRawType, originalLowerBound[0]); + if (lowerBound != originalLowerBound[0]) { + return new WildcardTypeImpl(new Type[] { Object.class }, new Type[] { lowerBound }); + } + } else if (originalUpperBound.length == 1) { + Type upperBound = resolve(context, contextRawType, originalUpperBound[0]); + if (upperBound != originalUpperBound[0]) { + return new WildcardTypeImpl(new Type[] { upperBound }, EMPTY_TYPE_ARRAY); + } + } + return original; + + } else { + return toResolve; + } + } + } + + private static Type resolveTypeVariable( + Type context, Class contextRawType, TypeVariable unknown) { + Class declaredByRaw = declaringClassOf(unknown); + + // We can't reduce this further. + if (declaredByRaw == null) return unknown; + + Type declaredBy = getGenericSupertype(context, contextRawType, declaredByRaw); + if (declaredBy instanceof ParameterizedType) { + int index = indexOf(declaredByRaw.getTypeParameters(), unknown); + return ((ParameterizedType) declaredBy).getActualTypeArguments()[index]; + } + + return unknown; + } + + /** + * Returns the declaring class of {@code typeVariable}, or {@code null} if it was not declared by + * a class. + */ + private static Class declaringClassOf(TypeVariable typeVariable) { + GenericDeclaration genericDeclaration = typeVariable.getGenericDeclaration(); + return genericDeclaration instanceof Class ? (Class) genericDeclaration : null; + } + + private static void checkNotPrimitive(Type type) { + if (type instanceof Class && ((Class) type).isPrimitive()) { + throw new IllegalArgumentException(); + } + } + + private static final class ParameterizedTypeImpl implements ParameterizedType { + private final Type ownerType; + private final Type rawType; + private final Type[] typeArguments; + + ParameterizedTypeImpl(Type ownerType, Type rawType, Type... typeArguments) { + // Require an owner type if the raw type needs it. + if (rawType instanceof Class + && (ownerType == null) != (((Class) rawType).getEnclosingClass() == null)) { + throw new IllegalArgumentException(); + } + + this.ownerType = ownerType; + this.rawType = rawType; + this.typeArguments = typeArguments.clone(); + + for (Type typeArgument : this.typeArguments) { + if (typeArgument == null) throw new NullPointerException(); + checkNotPrimitive(typeArgument); + } + } + + public Type[] getActualTypeArguments() { + return typeArguments.clone(); + } + + public Type getRawType() { + return rawType; + } + + public Type getOwnerType() { + return ownerType; + } + + @Override public boolean equals(Object other) { + return other instanceof ParameterizedType && Types.equals(this, (ParameterizedType) other); + } + + @Override public int hashCode() { + return Arrays.hashCode(typeArguments) ^ rawType.hashCode() ^ hashCodeOrZero(ownerType); + } + + @Override public String toString() { + StringBuilder result = new StringBuilder(30 * (typeArguments.length + 1)); + result.append(typeToString(rawType)); + if (typeArguments.length == 0) return result.toString(); + result.append("<").append(typeToString(typeArguments[0])); + for (int i = 1; i < typeArguments.length; i++) { + result.append(", ").append(typeToString(typeArguments[i])); + } + return result.append(">").toString(); + } + } + + private static final class GenericArrayTypeImpl implements GenericArrayType { + private final Type componentType; + + GenericArrayTypeImpl(Type componentType) { + this.componentType = componentType; + } + + public Type getGenericComponentType() { + return componentType; + } + + @Override public boolean equals(Object o) { + return o instanceof GenericArrayType + && Types.equals(this, (GenericArrayType) o); + } + + @Override public int hashCode() { + return componentType.hashCode(); + } + + @Override public String toString() { + return typeToString(componentType) + "[]"; + } + } + + /** + * The WildcardType interface supports multiple upper bounds and multiple + * lower bounds. We only support what the Java 6 language needs - at most one + * bound. If a lower bound is set, the upper bound must be Object.class. + */ + private static final class WildcardTypeImpl implements WildcardType { + private final Type upperBound; + private final Type lowerBound; + + WildcardTypeImpl(Type[] upperBounds, Type[] lowerBounds) { + if (lowerBounds.length > 1) throw new IllegalArgumentException(); + if (upperBounds.length != 1) throw new IllegalArgumentException(); + + if (lowerBounds.length == 1) { + if (lowerBounds[0] == null) throw new NullPointerException(); + checkNotPrimitive(lowerBounds[0]); + if (upperBounds[0] != Object.class) throw new IllegalArgumentException(); + this.lowerBound = lowerBounds[0]; + this.upperBound = Object.class; + } else { + if (upperBounds[0] == null) throw new NullPointerException(); + checkNotPrimitive(upperBounds[0]); + this.lowerBound = null; + this.upperBound = upperBounds[0]; + } + } + + public Type[] getUpperBounds() { + return new Type[] { upperBound }; + } + + public Type[] getLowerBounds() { + return lowerBound != null ? new Type[] { lowerBound } : EMPTY_TYPE_ARRAY; + } + + @Override public boolean equals(Object other) { + return other instanceof WildcardType && Types.equals(this, (WildcardType) other); + } + + @Override public int hashCode() { + // This equals Arrays.hashCode(getLowerBounds()) ^ Arrays.hashCode(getUpperBounds()). + return (lowerBound != null ? 31 + lowerBound.hashCode() : 1) ^ (31 + upperBound.hashCode()); + } + + @Override public String toString() { + if (lowerBound != null) return "? super " + typeToString(lowerBound); + if (upperBound == Object.class) return "?"; + return "? extends " + typeToString(upperBound); + } + } +} diff --git a/feign-core/src/main/java/feign/Util.java b/feign-core/src/main/java/feign/Util.java index edd6513d05..eceb6139a5 100644 --- a/feign-core/src/main/java/feign/Util.java +++ b/feign-core/src/main/java/feign/Util.java @@ -17,6 +17,9 @@ import java.io.IOException; import java.lang.reflect.Array; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; @@ -125,4 +128,30 @@ public static void ensureClosed(Response.Body body) { } } } + + /** + * Resolves the last type parameter of the parameterized {@code supertype}, based on the {@code genericContext}, + * into its upper bounds. + *

+ * Implementation copied from {@code retrofit.RestMethodInfo}. + * + * @param genericContext Ex. {@link java.lang.reflect.Field#getGenericType()} + * @param supertype Ex. {@code Decoder.class} + * @return in the example above, the type parameter of {@code Decoder}. + * @throws IllegalStateException if {@code supertype} cannot be resolved into a parameterized type using + * {@code context}. + */ + public static Type resolveLastTypeParameter(Type genericContext, Class supertype) throws IllegalStateException { + Type resolvedSuperType = Types.getSupertype(genericContext, Types.getRawType(genericContext), supertype); + checkState(resolvedSuperType instanceof ParameterizedType, "could not resolve %s into a parameterized type %s", + genericContext, supertype); + Type[] types = ParameterizedType.class.cast(resolvedSuperType).getActualTypeArguments(); + for (int i = 0; i < types.length; i++) { + Type type = types[i]; + if (type instanceof WildcardType) { + types[i] = ((WildcardType) type).getUpperBounds()[0]; + } + } + return types[types.length - 1]; + } } diff --git a/feign-core/src/main/java/feign/codec/BodyEncoder.java b/feign-core/src/main/java/feign/codec/BodyEncoder.java deleted file mode 100644 index 74f7c026eb..0000000000 --- a/feign-core/src/main/java/feign/codec/BodyEncoder.java +++ /dev/null @@ -1,55 +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.codec; - -import feign.RequestTemplate; - -public interface BodyEncoder { - /** - * Converts objects to an appropriate representation. Can affect any part of - * {@link RequestTemplate}. - *
- * Ex. - *
- *

-   * public class GsonEncoder implements BodyEncoder {
-   *   private final Gson gson;
-   *
-   *   public GsonEncoder(Gson gson) {
-   *     this.gson = gson;
-   *   }
-   *
-   *   @Override
-   *   public void encodeBody(Object bodyParam, RequestTemplate base) {
-   *     base.body(gson.toJson(bodyParam));
-   *   }
-   * }
-   * 
- *
- * If a parameter has no {@code *Param} annotation, it is passed to this - * method. - *
- *
-   * @POST
-   * @Path("/")
-   * void create(User user);
-   * 
- * - * @param bodyParam a body parameter - * @param base template to encode the {@code object} into. - */ - void encodeBody(Object bodyParam, RequestTemplate base); -} diff --git a/feign-core/src/main/java/feign/codec/DecodeException.java b/feign-core/src/main/java/feign/codec/DecodeException.java new file mode 100644 index 0000000000..5efab25ba6 --- /dev/null +++ b/feign-core/src/main/java/feign/codec/DecodeException.java @@ -0,0 +1,45 @@ +/* + * 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.codec; + +import feign.FeignException; + +import static feign.Util.checkNotNull; + +/** + * Similar to {@code javax.websocket.DecodeException}, raised when a problem + * occurs decoding a message. Note that {@code DecodeException} is not an + * {@code IOException}, nor have one set as its cause. + */ +public class DecodeException extends FeignException { + + /** + * @param message the reason for the failure. + */ + public DecodeException(String message) { + super(checkNotNull(message, "message")); + } + + /** + * @param message the reason for the failure. + * @param cause the cause of the error. + */ + public DecodeException(String message, Throwable cause) { + super(checkNotNull(message, "message"), checkNotNull(cause, "cause")); + } + + private static final long serialVersionUID = 1L; +} diff --git a/feign-core/src/main/java/feign/codec/Decoder.java b/feign-core/src/main/java/feign/codec/Decoder.java index 98cefdaa91..afcc6406ee 100644 --- a/feign-core/src/main/java/feign/codec/Decoder.java +++ b/feign-core/src/main/java/feign/codec/Decoder.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -19,32 +19,17 @@ import java.io.Reader; import java.lang.reflect.Type; +import feign.FeignException; import feign.Response; -import static feign.Util.ensureClosed; - /** * Decodes an HTTP response into a given type. Invoked when - * {@link Response#status()} is in the 2xx range. - *
- * Ex. + * {@link Response#status()} is in the 2xx range. Like + * {@code javax.websocket.Decoder}, except that the decode method is passed the + * generic type of the target.
*
- *
- * public class GsonDecoder extends Decoder {
- *   private final Gson gson;
- *
- *   public GsonDecoder(Gson gson) {
- *     this.gson = gson;
- *   }
- *
- *   @Override
- *   public Object decode(Reader reader, Type type) {
- *     return gson.fromJson(reader, type);
- *   }
- * }
- * 
*
- *

Error handling
+ * Error handling
*
* Responses where {@link Response#status()} is not in the 2xx range are * classified as errors, addressed by the {@link ErrorDecoder}. That said, @@ -53,42 +38,56 @@ * is returned with a 200 status, encoded in json. When scenarios like this * occur, you should raise an application-specific exception (which may be * {@link feign.RetryableException retryable}). + * + * @param input that can be derived from {@link feign.Response.Body}. + * @param widest type an instance of this can decode. */ -public abstract class Decoder { - +public interface Decoder { /** - * Override this method in order to consider the HTTP {@link Response} as - * opposed to just the {@link feign.Response.Body} when decoding into a new - * instance of {@code type}. + * Implement this to decode a resource to an object of the specified type. + * If you need to wrap exceptions, please do so via {@link DecodeException}. * - * @param response HTTP response. + * @param input if {@code Closeable}, no need to close this, as the caller + * manages resources. * @param type Target object type. * @return instance of {@code type} - * @throws IOException if there was a network error reading the response. - * @throws Exception if the decoder threw a checked exception. + * @throws IOException will be propagated safely to the caller. + * @throws DecodeException when decoding failed due to a checked exception + * besides IOException. + * @throws FeignException when decoding succeeds, but conveys the operation + * failed. */ - public Object decode(Response response, Type type) throws Exception { - Response.Body body = response.body(); - if (body == null) - return null; - Reader reader = body.asReader(); - try { - return decode(reader, type); - } finally { - ensureClosed(body); - } - } + T decode(I input, Type type) throws IOException, DecodeException, FeignException; /** - * Implement this to decode a {@code Reader} to an object of the specified - * type. + * Used for text-based apis, follows + * {@link Decoder#decode(Object, java.lang.reflect.Type)} + * semantics, applied to inputs of type {@link java.io.Reader}.
+ * Ex.
+ *

+ *

+   * public class GsonDecoder implements Decoder.TextStream<Object> {
+   *   private final Gson gson;
    *
-   * @param reader    no need to close this, as {@link #decode(Response, Type)}
-   *                  manages resources.
-   * @param type      Target object type.
-   * @return instance of {@code type}
-   * @throws IOException will be propagated safely to the caller.
-   * @throws Exception if the decoder threw a checked exception.
+   *   public GsonDecoder(Gson gson) {
+   *     this.gson = gson;
+   *   }
+   *
+   *   @Override
+   *   public Object decode(Reader reader, Type type) throws IOException {
+   *     try {
+   *       return gson.fromJson(reader, type);
+   *     } catch (JsonIOException e) {
+   *       if (e.getCause() != null &&
+   *           e.getCause() instanceof IOException) {
+   *         throw IOException.class.cast(e.getCause());
+   *       }
+   *       throw e;
+   *     }
+   *   }
+   * }
+   * 
*/ - public abstract Object decode(Reader reader, Type type) throws Exception; + public interface TextStream extends Decoder { + } } diff --git a/feign-core/src/main/java/feign/codec/Decoders.java b/feign-core/src/main/java/feign/codec/Decoders.java index 22ca4f7dcb..80b61ce999 100644 --- a/feign-core/src/main/java/feign/codec/Decoders.java +++ b/feign-core/src/main/java/feign/codec/Decoders.java @@ -29,9 +29,10 @@ import static java.util.regex.Pattern.compile; /** - * Static utility methods pertaining to {@code Decoder} instances. + * Static utility methods pertaining to {@code Decoder} instances.
*
- *

Pattern Decoders
+ *
+ * Pattern Decoders
*
* Pattern decoders typically require less initialization, dependencies, and * code than reflective decoders, but not can be awkward to those unfamiliar @@ -53,106 +54,143 @@ public interface ApplyFirstGroup { } /** - * The first match group is applied to {@code applyGroups} and result - * returned. If no matches are found, the response is null; - *
- * Ex. to pull the first interesting element from an xml response: + * shortcut for
new TransformFirstGroup(pattern, applyFirstGroup){}
when + * {@code String} is the type you are decoding into.
*
+ * Ex. to pull the first interesting element from an xml response:
+ *

*

-   * decodeFirstDirPoolID = transformFirstGroup("<DirPoolID[ˆ>]*>([ˆ<]+)</DirPoolID>", ToLong.INSTANCE);
+   * decodeFirstDirPoolID = firstGroup("<DirPoolID[ˆ>]*>([ˆ<]+)</DirPoolID>");
    * 
*/ - public static Decoder transformFirstGroup(String pattern, final ApplyFirstGroup applyFirstGroup) { - final Pattern patternForMatcher = compile(checkNotNull(pattern, "pattern"), DOTALL); - checkNotNull(applyFirstGroup, "applyFirstGroup"); - return new Decoder() { - @Override - public Object decode(Reader reader, Type type) throws IOException { - Matcher matcher = patternForMatcher.matcher(Decoders.toString(reader)); - if (matcher.find()) { - return applyFirstGroup.apply(matcher.group(1)); - } - return null; - } - - @Override public String toString() { - return format("decode groups from %s into %s", patternForMatcher, applyFirstGroup); - } + public static Decoder.TextStream firstGroup(String pattern) { + return new TransformFirstGroup(pattern, IDENTITY) { }; } /** - * shortcut for {@link Decoders#transformFirstGroup(String, ApplyFirstGroup)} when - * {@code String} is the type you are decoding into. - *
- *
- * Ex. to pull the first interesting element from an xml response: - *
+ * shortcut for
new TransformEachFirstGroup(pattern, applyFirstGroup){}
when + * {@code List} is the type you are decoding into.
+ * Ex. to pull a list zones names, which are http paths starting with + * {@code /Rest/Zone/}:
+ *

*

-   * decodeFirstDirPoolID = firstGroup("<DirPoolID[ˆ>]*>([ˆ<]+)</DirPoolID>");
+   * decodeListOfZonesNames = eachFirstGroup("/REST/Zone/([ˆ/]+)/");
    * 
*/ - public static Decoder firstGroup(String pattern) { - return transformFirstGroup(pattern, IDENTITY); + public static Decoder.TextStream> eachFirstGroup(String pattern) { + return new TransformEachFirstGroup(pattern, IDENTITY) { + }; } + private static String toString(Reader reader) throws IOException { + return TO_STRING.decode(reader, null).toString(); + } + + private static final StringDecoder TO_STRING = new StringDecoder(); + + private static final ApplyFirstGroup IDENTITY = new ApplyFirstGroup() { + @Override + public String apply(String firstGroup) { + return firstGroup; + } + }; + /** - * On the each find the first match group is applied to - * {@code applyFirstGroup} and added to the list returned. If no matches are - * found, the response is an empty list; - *
- * Ex. to pull a list zones constructed from http paths starting with - * {@code /Rest/Zone/}: - *
+ * The first match group is applied to {@code applyGroups} and result + * returned. If no matches are found, the response is null;
+ * Ex. to pull the first interesting element from an xml response:
+ *

*

-   * decodeListOfZones = transformEachFirstGroup("/REST/Zone/([ˆ/]+)/", ToZone.INSTANCE);
+   * decodeFirstDirPoolID = new TransformFirstGroup<Long>("<DirPoolID[ˆ>]*>([ˆ<]+)</DirPoolID>", ToLong.INSTANCE) {
+   * };
    * 
*/ - public static Decoder transformEachFirstGroup(String pattern, final ApplyFirstGroup applyFirstGroup) { - final Pattern patternForMatcher = compile(checkNotNull(pattern, "pattern"), DOTALL); - checkNotNull(applyFirstGroup, "applyFirstGroup"); - return new Decoder() { - @Override - public List decode(Reader reader, Type type) throws IOException { - Matcher matcher = patternForMatcher.matcher(Decoders.toString(reader)); - List result = new ArrayList(); - while (matcher.find()) { - result.add(applyFirstGroup.apply(matcher.group(1))); - } - return result; - } + public static class TransformFirstGroup implements Decoder.TextStream { + private final Pattern patternForMatcher; + private final ApplyFirstGroup applyFirstGroup; + + /** + * You must subclass this, in order to prevent type erasure on {@code T} + * . In addition to making a concrete type, you can also use the + * following form. + *

+ *
+ *

+ *

+     * new TransformFirstGroup<Foo>(pattern, applyFirstGroup) {
+     * }; // note the curly braces ensures no type erasure!
+     * 
+ */ + protected TransformFirstGroup(String pattern, ApplyFirstGroup applyFirstGroup) { + this.patternForMatcher = compile(checkNotNull(pattern, "pattern"), DOTALL); + this.applyFirstGroup = checkNotNull(applyFirstGroup, "applyFirstGroup"); + } - @Override public String toString() { - return format("decode %s into list elements, where each group(1) is transformed with %s", - patternForMatcher, applyFirstGroup); + @Override + public T decode(Reader reader, Type type) throws IOException { + Matcher matcher = patternForMatcher.matcher(Decoders.toString(reader)); + if (matcher.find()) { + return applyFirstGroup.apply(matcher.group(1)); } - }; + return null; + } + + @Override + public String toString() { + return format("decode groups from %s into %s", patternForMatcher, applyFirstGroup); + } } /** - * shortcut for {@link Decoders#transformEachFirstGroup(String, ApplyFirstGroup)} - * when {@code List} is the type you are decoding into. - *
- * Ex. to pull a list zones names, which are http paths starting with + * On the each find the first match group is applied to + * {@code applyFirstGroup} and added to the list returned. If no matches are + * found, the response is an empty list;
+ * Ex. to pull a list zones constructed from http paths starting with * {@code /Rest/Zone/}: + *

*
+ *

*

-   * decodeListOfZonesNames = eachFirstGroup("/REST/Zone/([ˆ/]+)/");
+   * decodeListOfZones = new TransformEachFirstGroup("/REST/Zone/([ˆ/]+)/", ToZone.INSTANCE) {
+   * };
    * 
*/ - public static Decoder eachFirstGroup(String pattern) { - return transformEachFirstGroup(pattern, IDENTITY); - } + public static class TransformEachFirstGroup implements Decoder.TextStream> { + private final Pattern patternForMatcher; + private final ApplyFirstGroup applyFirstGroup; - private static String toString(Reader reader) throws IOException { - return TO_STRING.decode(reader, null).toString(); - } + /** + * You must subclass this, in order to prevent type erasure on {@code T} + * . In addition to making a concrete type, you can also use the + * following form. + *

+ *
+ *

+ *

+     * new TransformEachFirstGroup<Foo>(pattern, applyFirstGroup) {
+     * }; // note the curly braces ensures no type erasure!
+     * 
+ */ + protected TransformEachFirstGroup(String pattern, ApplyFirstGroup applyFirstGroup) { + this.patternForMatcher = compile(checkNotNull(pattern, "pattern"), DOTALL); + this.applyFirstGroup = checkNotNull(applyFirstGroup, "applyFirstGroup"); + } - private static final StringDecoder TO_STRING = new StringDecoder(); + @Override + public List decode(Reader reader, Type type) throws IOException { + Matcher matcher = patternForMatcher.matcher(Decoders.toString(reader)); + List result = new ArrayList(); + while (matcher.find()) { + result.add(applyFirstGroup.apply(matcher.group(1))); + } + return result; + } - private static final ApplyFirstGroup IDENTITY = new ApplyFirstGroup() { - @Override public String apply(String firstGroup) { - return firstGroup; + @Override + public String toString() { + return format("decode %s into list elements, where each group(1) is transformed with %s", + patternForMatcher, applyFirstGroup); } - }; + } } diff --git a/feign-core/src/main/java/feign/codec/EncodeException.java b/feign-core/src/main/java/feign/codec/EncodeException.java new file mode 100644 index 0000000000..12d06ba340 --- /dev/null +++ b/feign-core/src/main/java/feign/codec/EncodeException.java @@ -0,0 +1,45 @@ +/* + * 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.codec; + +import feign.FeignException; + +import static feign.Util.checkNotNull; + +/** + * Similar to {@code javax.websocket.EncodeException}, raised when a problem + * occurs decoding a message. Note that {@code DecodeException} is not an + * {@code IOException}, nor have one set as its cause. + */ +public class EncodeException extends FeignException { + + /** + * @param message the reason for the failure. + */ + public EncodeException(String message) { + super(checkNotNull(message, "message")); + } + + /** + * @param message the reason for the failure. + * @param cause the cause of the error. + */ + public EncodeException(String message, Throwable cause) { + super(checkNotNull(message, "message"), checkNotNull(cause, "cause")); + } + + private static final long serialVersionUID = 1L; +} diff --git a/feign-core/src/main/java/feign/codec/Encoder.java b/feign-core/src/main/java/feign/codec/Encoder.java new file mode 100644 index 0000000000..80614b53fd --- /dev/null +++ b/feign-core/src/main/java/feign/codec/Encoder.java @@ -0,0 +1,75 @@ +/* + * 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.codec; + +/** + * Encodes an object into an HTTP request body. Like + * {@code javax.websocket.Encoder}.
+ * {@code Encoder} is used when a method parameter has no {@code *Param} + * annotation. For example:
+ *

+ *

+ * @POST
+ * @Path("/")
+ * void create(User user);
+ * 
+ *

+ *

Form encoding

+ *
+ * If any parameters are found in {@link feign.MethodMetadata#formParams()}, they will be + * collected and passed to {@code Encoder.Text>}. + *
+ *
+ * @POST
+ * @Path("/")
+ * Session login(@Named("username") String username, @Named("password") String password);
+ * 
+ * + * @param widest type an instance of this can encode. + */ +public interface Encoder { + + /** + * Converts objects to an appropriate text representation.
+ * Ex.
+ *

+ *

+   * public class GsonEncoder implements Encoder.Text<Object> {
+   *     private final Gson gson;
+   *
+   *     public GsonEncoder(Gson gson) {
+   *         this.gson = gson;
+   *     }
+   *
+   *     @Override
+   *     public String encode(Object object) {
+   *         return gson.toJson(object);
+   *     }
+   * }
+   * 
+ */ + interface Text extends Encoder { + /** + * Implement this to encode an object as a String.. If you need to wrap + * exceptions, please do so via {@link EncodeException} + * + * @param object what to encode as the request body. + * @return the encoded object as a string. * @throws EncodeException + * when encoding failed due to a checked exception. + */ + String encode(T object) throws EncodeException; + } +} diff --git a/feign-core/src/main/java/feign/codec/ErrorDecoder.java b/feign-core/src/main/java/feign/codec/ErrorDecoder.java index 169935042a..d9982360e5 100644 --- a/feign-core/src/main/java/feign/codec/ErrorDecoder.java +++ b/feign-core/src/main/java/feign/codec/ErrorDecoder.java @@ -68,7 +68,7 @@ public interface ErrorDecoder { */ public Exception decode(String methodKey, Response response); - public static final ErrorDecoder DEFAULT = new ErrorDecoder() { + public static class Default implements ErrorDecoder { private final RetryAfterDecoder retryAfterDecoder = new RetryAfterDecoder(); @@ -87,7 +87,7 @@ private T firstOrNull(Map> map, String key) { } return null; } - }; + } /** * Decodes a {@link feign.Util#RETRY_AFTER} header into an absolute date, diff --git a/feign-core/src/main/java/feign/codec/FormEncoder.java b/feign-core/src/main/java/feign/codec/FormEncoder.java deleted file mode 100644 index 381f80c2d8..0000000000 --- a/feign-core/src/main/java/feign/codec/FormEncoder.java +++ /dev/null @@ -1,40 +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.codec; - -import java.util.Map; - -import feign.RequestTemplate; - -public interface FormEncoder { - - /** - * FormParam encoding - *
- * If any parameters are found in {@link feign.MethodMetadata#formParams()}, they will be - * collected and passed as {code formParams} - *
- *
-   * @POST
-   * @Path("/")
-   * Session login(@FormParam("username") String username, @FormParam("password") String password);
-   * 
- * - * @param formParams Object instance to convert. - * @param base template to encode the {@code object} into. - */ - void encodeForm(Map formParams, RequestTemplate base); -} diff --git a/feign-core/src/main/java/feign/codec/SAXDecoder.java b/feign-core/src/main/java/feign/codec/SAXDecoder.java index 25cf8efc9c..972fee9cc2 100644 --- a/feign-core/src/main/java/feign/codec/SAXDecoder.java +++ b/feign-core/src/main/java/feign/codec/SAXDecoder.java @@ -19,46 +19,56 @@ import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; +import org.xml.sax.helpers.XMLReaderFactory; +import javax.inject.Provider; import java.io.IOException; import java.io.Reader; import java.lang.reflect.Type; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.parsers.SAXParserFactory; - import static feign.Util.checkNotNull; import static feign.Util.checkState; -public abstract class SAXDecoder extends Decoder { +public class SAXDecoder implements Decoder.TextStream { /* Implementations are not intended to be shared across requests. */ - public interface ContentHandlerWithResult extends ContentHandler { - /* expected to be set following a call to {@link XMLReader#parse(InputSource)} */ - Object getResult(); + public interface ContentHandlerWithResult extends ContentHandler { + /* + * expected to be set following a call to {@link + * XMLReader#parse(InputSource)} + */ + T result(); } - private final SAXParserFactory factory; - - protected SAXDecoder() { - this(SAXParserFactory.newInstance()); - factory.setNamespaceAware(false); - factory.setValidating(false); - } + private final Provider> handlers; - protected SAXDecoder(SAXParserFactory factory) { - this.factory = checkNotNull(factory, "factory"); + /** + * You must subclass this, in order to prevent type erasure on {@code T}. In + * addition to making a concrete type, you can also use the following form. + *

+ *
+ *

+ *

+   * new SaxDecoder<Foo>(fooHandlers) {
+   * }; // note the curly braces ensures no type erasure!
+   * 
+ */ + protected SAXDecoder(Provider> handlers) { + this.handlers = checkNotNull(handlers, "handlers"); } @Override - public Object decode(Reader reader, Type type) throws IOException, SAXException, ParserConfigurationException { - ContentHandlerWithResult handler = typeToNewHandler(type); + public T decode(Reader reader, Type type) throws IOException, DecodeException { + ContentHandlerWithResult handler = handlers.get(); checkState(handler != null, "%s returned null for type %s", this, type); - XMLReader xmlReader = factory.newSAXParser().getXMLReader(); - xmlReader.setContentHandler(handler); - InputSource source = new InputSource(reader); - xmlReader.parse(source); - return handler.getResult(); + try { + XMLReader xmlReader = XMLReaderFactory.createXMLReader(); + xmlReader.setFeature("http://xml.org/sax/features/namespaces", false); + xmlReader.setFeature("http://xml.org/sax/features/validation", false); + xmlReader.setContentHandler(handler); + xmlReader.parse(new InputSource(reader)); + return handler.result(); + } catch (SAXException e) { + throw new DecodeException(e.getMessage(), e); + } } - - protected abstract ContentHandlerWithResult typeToNewHandler(Type type); } diff --git a/feign-core/src/main/java/feign/codec/StringDecoder.java b/feign-core/src/main/java/feign/codec/StringDecoder.java index 3e34fc2b59..8711b2d424 100644 --- a/feign-core/src/main/java/feign/codec/StringDecoder.java +++ b/feign-core/src/main/java/feign/codec/StringDecoder.java @@ -20,32 +20,14 @@ import java.lang.reflect.Type; import java.nio.CharBuffer; -import feign.Response; - -import static feign.Util.ensureClosed; - /** * Adapted from {@code com.google.common.io.CharStreams.toString()}. */ -public class StringDecoder extends Decoder { +public class StringDecoder implements Decoder.TextStream { private static final int BUF_SIZE = 0x800; // 2K chars (4K bytes) - // overridden to throw only IOException - @Override - public Object decode(Response response, Type type) throws IOException { - Response.Body body = response.body(); - if (body == null) - return null; - Reader reader = body.asReader(); - try { - return decode(reader, type); - } finally { - ensureClosed(body); - } - } - @Override - public Object decode(Reader from, Type type) throws IOException { + public String decode(Reader from, Type type) throws IOException { StringBuilder to = new StringBuilder(); CharBuffer buf = CharBuffer.allocate(BUF_SIZE); while (from.read(buf) != -1) { diff --git a/feign-core/src/test/java/feign/DefaultContractTest.java b/feign-core/src/test/java/feign/DefaultContractTest.java index 8fc0dc2b71..dc66330073 100644 --- a/feign-core/src/test/java/feign/DefaultContractTest.java +++ b/feign-core/src/test/java/feign/DefaultContractTest.java @@ -17,12 +17,12 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; - +import com.google.gson.reflect.TypeToken; import org.testng.annotations.Test; -import java.net.URI; - import javax.inject.Named; +import java.net.URI; +import java.util.List; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; @@ -30,13 +30,13 @@ import static org.testng.Assert.assertTrue; /** - * Tests interfaces defined per {@link feign.Contract.DefaultContract} are interpreted into expected {@link feign + * Tests interfaces defined per {@link feign.Contract.Default} are interpreted into expected {@link feign * .RequestTemplate template} * instances. */ @Test public class DefaultContractTest { - Contract.DefaultContract contract = new Contract.DefaultContract(); + Contract.Default contract = new Contract.Default(); interface Methods { @RequestLine("POST /") void post(); @@ -59,6 +59,28 @@ interface Methods { "DELETE"); } + interface BodyParams { + @RequestLine("POST") Response post(List body); + + @RequestLine("POST") Response tooMany(List body, List body2); + } + + @Test public void bodyParamIsGeneric() throws Exception { + MethodMetadata md = contract.parseAndValidatateMetadata(BodyParams.class.getDeclaredMethod("post", + List.class)); + assertNull(md.template().body()); + assertNull(md.template().bodyTemplate()); + assertNull(md.urlIndex()); + assertEquals(md.bodyIndex(), Integer.valueOf(0)); + assertEquals(md.bodyType(), new TypeToken>() { + }.getType()); + } + + @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "Method has too many Body.*") + public void tooManyBodies() throws Exception { + contract.parseAndValidatateMetadata(BodyParams.class.getDeclaredMethod("tooMany", List.class, List.class)); + } + interface CustomMethodAndURIParam { @RequestLine("PATCH") Response patch(URI nextLink); } diff --git a/feign-core/src/test/java/feign/FeignTest.java b/feign-core/src/test/java/feign/FeignTest.java index bfb4eadeba..3155e1549f 100644 --- a/feign-core/src/test/java/feign/FeignTest.java +++ b/feign-core/src/test/java/feign/FeignTest.java @@ -15,32 +15,36 @@ */ 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 com.google.mockwebserver.SocketPolicy; - +import dagger.Module; +import dagger.Provides; +import feign.codec.Decoder; +import feign.codec.EncodeException; +import feign.codec.Encoder; +import feign.codec.ErrorDecoder; +import feign.codec.StringDecoder; import org.testng.annotations.Test; +import javax.inject.Named; +import javax.inject.Singleton; +import javax.net.ssl.SSLSocketFactory; import java.io.IOException; import java.io.Reader; import java.lang.reflect.Type; import java.net.URI; +import java.util.Arrays; +import java.util.List; import java.util.Map; -import javax.inject.Named; -import javax.inject.Singleton; -import javax.net.ssl.SSLSocketFactory; - -import dagger.Module; -import dagger.Provides; -import feign.codec.Decoder; -import feign.codec.ErrorDecoder; -import feign.codec.StringDecoder; - +import static dagger.Provides.Type.SET; import static org.testng.Assert.assertEquals; @Test +// unbound wildcards are not currently injectable in dagger. +@SuppressWarnings("rawtypes") public class FeignTest { interface TestInterface { @RequestLine("POST /") String post(); @@ -48,17 +52,33 @@ interface TestInterface { @RequestLine("POST /") @Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D") void login( - @Named("customer_name") String customer, - @Named("user_name") String user, @Named("password") String password); + @Named("customer_name") String customer, @Named("user_name") String user, @Named("password") String password); + + @RequestLine("POST /") + void body(List 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); @dagger.Module(overrides = true, library = true) static class Module { - // until dagger supports real map binding, we need to recreate the - // entire map, as opposed to overriding a single entry. - @Provides @Singleton Map decoders() { - return ImmutableMap.of("TestInterface", new StringDecoder()); + @Provides(type = SET) Encoder defaultEncoder() { + return new Encoder.Text() { + @Override public String encode(Object object) { + return object.toString(); + } + }; + } + + @Provides(type = SET) Encoder formEncoder() { + return new Encoder.Text>() { + @Override public String encode(Map object) { + return Joiner.on(',').withKeyValueSeparator("=").join(object); + } + }; } } } @@ -80,6 +100,39 @@ public void postTemplateParamsResolve() throws IOException, InterruptedException } } + @Test + public void postFormParams() throws IOException, InterruptedException { + final MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse().setResponseCode(200).setBody("foo")); + server.play(); + + try { + TestInterface api = Feign.create(TestInterface.class, server.getUrl("").toString(), new TestInterface.Module()); + + api.form("netflix", "denominator", "password"); + assertEquals(new String(server.takeRequest().getBody()), + "customer_name=netflix,user_name=denominator,password=password"); + } finally { + server.shutdown(); + } + } + + @Test + public void postBodyParam() throws IOException, InterruptedException { + final MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse().setResponseCode(200).setBody("foo")); + server.play(); + + try { + TestInterface api = Feign.create(TestInterface.class, server.getUrl("").toString(), new TestInterface.Module()); + + api.body(Arrays.asList("netflix", "denominator", "password")); + assertEquals(new String(server.takeRequest().getBody()), "[netflix, denominator, password]"); + } finally { + server.shutdown(); + } + } + @Test public void toKeyMethodFormatsAsExpected() throws Exception { assertEquals(Feign.configKey(TestInterface.class.getDeclaredMethod("post")), "TestInterface#post()"); assertEquals(Feign.configKey(TestInterface.class.getDeclaredMethod("uriParam", String.class, URI.class, @@ -87,19 +140,19 @@ public void postTemplateParamsResolve() throws IOException, InterruptedException } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "zone not found") - public void canOverrideErrorDecoderOnMethod() throws IOException, InterruptedException { + public void canOverrideErrorDecoder() throws IOException, InterruptedException { @dagger.Module(overrides = true, includes = TestInterface.Module.class) class Overrides { - @Provides @Singleton Map decoders() { - return ImmutableMap.of("TestInterface#post()", new ErrorDecoder() { + @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 ErrorDecoder.DEFAULT.decode(methodKey, response); + return super.decode(methodKey, response); } - }); + }; } } @@ -134,6 +187,63 @@ public Exception decode(String methodKey, Response response) { } } + 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() { + @Override + public String decode(Reader reader, Type type) throws IOException { + return "fail"; + } + }; + } + } + TestInterface api = Feign.create(TestInterface.class, server.getUrl("").toString(), new Overrides()); + + assertEquals(api.post(), "fail"); + } finally { + server.shutdown(); + assertEquals(server.getRequestCount(), 1); + } + } + + /** + * 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.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()); + + assertEquals(api.post(), "success!"); + } finally { + server.shutdown(); + assertEquals(server.getRequestCount(), 2); + } + } + @Test(expectedExceptions = FeignException.class, expectedExceptionsMessageRegExp = "error reading response POST http://.*") public void doesntRetryAfterResponseIsSent() throws IOException, InterruptedException { MockWebServer server = new MockWebServer(); @@ -141,16 +251,14 @@ public void doesntRetryAfterResponseIsSent() throws IOException, InterruptedExce server.play(); try { - @dagger.Module(overrides = true) class Overrides { - @Provides @Singleton Map decoders() { - return ImmutableMap.of("TestInterface", new Decoder() { - + @dagger.Module(overrides = true, includes = TestInterface.Module.class) class Overrides { + @Provides(type = SET) Decoder decoder() { + return new Decoder.TextStream() { @Override - public Object decode(Reader reader, Type type) throws IOException { + 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()); diff --git a/feign-core/src/test/java/feign/LoggerTest.java b/feign-core/src/test/java/feign/LoggerTest.java index 22ccc041b3..d72d89cfa9 100644 --- a/feign-core/src/test/java/feign/LoggerTest.java +++ b/feign-core/src/test/java/feign/LoggerTest.java @@ -20,6 +20,7 @@ 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; @@ -33,6 +34,7 @@ import java.util.List; import java.util.Map; +import static dagger.Provides.Type.SET; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; @@ -105,8 +107,12 @@ public void levelEmits(final Logger.Level logLevel, List expectedMessage server.play(); @dagger.Module(overrides = true, library = true) class Module { - @Provides @Singleton Map decoders() { - return ImmutableMap.of("SendsStuff", new StringDecoder()); + @Provides(type = SET) Encoder defaultEncoder() { + return new Encoder.Text() { + @Override public String encode(Object object) { + return object.toString(); + } + }; } @Provides @Singleton Logger logger() { diff --git a/feign-core/src/test/java/feign/UtilTest.java b/feign-core/src/test/java/feign/UtilTest.java new file mode 100644 index 0000000000..63a2e9ae22 --- /dev/null +++ b/feign-core/src/test/java/feign/UtilTest.java @@ -0,0 +1,92 @@ +/* + * 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 feign.codec.Decoder; +import feign.codec.Decoders; +import feign.codec.StringDecoder; +import org.testng.annotations.Test; + +import java.io.Reader; +import java.lang.reflect.Type; +import java.util.List; + +import static feign.Util.resolveLastTypeParameter; +import static org.testng.Assert.assertEquals; + +@Test +public class UtilTest { + + interface LastTypeParameter { + final List LIST_STRING = null; + final Decoder.TextStream> DECODER_LIST_STRING = null; + final Decoder.TextStream> DECODER_WILDCARD_LIST_STRING = null; + final ParameterizedDecoder> PARAMETERIZED_DECODER_LIST_STRING = null; + final ParameterizedDecoder PARAMETERIZED_DECODER_UNBOUND = null; + } + + interface ParameterizedDecoder> extends Decoder.TextStream { + } + + @Test public void resolveLastTypeParameterWhenNotSubtype() throws Exception { + Type context = LastTypeParameter.class.getDeclaredField("DECODER_LIST_STRING").getGenericType(); + Type listStringType = LastTypeParameter.class.getDeclaredField("LIST_STRING").getGenericType(); + Type last = resolveLastTypeParameter(context, Decoder.class); + assertEquals(last, listStringType); + } + + @Test public void lastTypeFromInstance() throws Exception { + Decoder.TextStream decoder = new StringDecoder(); + Type last = resolveLastTypeParameter(decoder.getClass(), Decoder.class); + assertEquals(last, String.class); + } + + @Test public void lastTypeFromStaticMethod() throws Exception { + Decoder.TextStream decoder = Decoders.firstGroup("foo"); + Type last = resolveLastTypeParameter(decoder.getClass(), Decoder.class); + assertEquals(last, String.class); + } + + @Test public void lastTypeFromAnonymous() throws Exception { + Decoder.TextStream decoder = new Decoder.TextStream() { + @Override public Reader decode(Reader reader, Type type) { + return null; + } + }; + Type last = resolveLastTypeParameter(decoder.getClass(), Decoder.class); + assertEquals(last, Reader.class); + } + + @Test public void resolveLastTypeParameterWhenWildcard() throws Exception { + Type context = LastTypeParameter.class.getDeclaredField("DECODER_WILDCARD_LIST_STRING").getGenericType(); + Type listStringType = LastTypeParameter.class.getDeclaredField("LIST_STRING").getGenericType(); + Type last = resolveLastTypeParameter(context, Decoder.class); + assertEquals(last, listStringType); + } + + @Test public void resolveLastTypeParameterWhenParameterizedSubtype() throws Exception { + Type context = LastTypeParameter.class.getDeclaredField("PARAMETERIZED_DECODER_LIST_STRING").getGenericType(); + Type listStringType = LastTypeParameter.class.getDeclaredField("LIST_STRING").getGenericType(); + Type last = resolveLastTypeParameter(context, ParameterizedDecoder.class); + assertEquals(last, listStringType); + } + + @Test public void unboundWildcardIsObject() throws Exception { + Type context = LastTypeParameter.class.getDeclaredField("PARAMETERIZED_DECODER_UNBOUND").getGenericType(); + Type last = resolveLastTypeParameter(context, ParameterizedDecoder.class); + assertEquals(last, Object.class); + } +} diff --git a/feign-core/src/test/java/feign/codec/DefaultErrorDecoderTest.java b/feign-core/src/test/java/feign/codec/DefaultErrorDecoderTest.java index bd3b17835f..efab2c9a76 100644 --- a/feign-core/src/test/java/feign/codec/DefaultErrorDecoderTest.java +++ b/feign-core/src/test/java/feign/codec/DefaultErrorDecoderTest.java @@ -29,12 +29,14 @@ import static feign.Util.RETRY_AFTER; public class DefaultErrorDecoderTest { + ErrorDecoder errorDecoder = new ErrorDecoder.Default(); + @Test(expectedExceptions = FeignException.class, expectedExceptionsMessageRegExp = "status 500 reading Service#foo\\(\\)") public void throwsFeignException() throws Throwable { Response response = Response.create(500, "Internal server error", ImmutableMap.>of(), null); - throw ErrorDecoder.DEFAULT.decode("Service#foo()", response); + throw errorDecoder.decode("Service#foo()", response); } @Test(expectedExceptions = FeignException.class, expectedExceptionsMessageRegExp = "status 500 reading Service#foo\\(\\); content:\nhello world") @@ -42,7 +44,7 @@ public void throwsFeignExceptionIncludingBody() throws Throwable { Response response = Response.create(500, "Internal server error", ImmutableMap.>of(), "hello world"); - throw ErrorDecoder.DEFAULT.decode("Service#foo()", response); + throw errorDecoder.decode("Service#foo()", response); } @Test(expectedExceptions = RetryableException.class, expectedExceptionsMessageRegExp = "status 503 reading Service#foo\\(\\)") @@ -50,6 +52,6 @@ public void retryAfterHeaderThrowsRetryableException() throws Throwable { Response response = Response.create(503, "Service Unavailable", ImmutableMultimap.of(RETRY_AFTER, "Sat, 1 Jan 2000 00:00:00 GMT").asMap(), null); - throw ErrorDecoder.DEFAULT.decode("Service#foo()", response); + throw errorDecoder.decode("Service#foo()", response); } } diff --git a/feign-core/src/test/java/feign/examples/GitHubExample.java b/feign-core/src/test/java/feign/examples/GitHubExample.java index 61b317e7d9..502f0f3e22 100644 --- a/feign-core/src/test/java/feign/examples/GitHubExample.java +++ b/feign-core/src/test/java/feign/examples/GitHubExample.java @@ -15,29 +15,25 @@ */ package feign.examples; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.collect.ImmutableMap; import com.google.gson.Gson; - -import java.io.IOException; -import java.io.Reader; -import java.lang.reflect.Type; -import java.util.List; -import java.util.Map; - -import javax.inject.Named; -import javax.inject.Singleton; - +import com.google.gson.JsonIOException; import dagger.Module; import dagger.Provides; import feign.Feign; import feign.RequestLine; import feign.codec.Decoder; +import javax.inject.Named; +import java.io.IOException; +import java.io.Reader; +import java.lang.reflect.Type; +import java.util.List; + import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY; import static com.fasterxml.jackson.annotation.PropertyAccessor.FIELD; import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; +import static dagger.Provides.Type.SET; /** * adapted from {@code com.example.retrofit.GitHubClient} @@ -69,17 +65,22 @@ public static void main(String... args) { */ @Module(overrides = true, library = true) static class GsonModule { - @Provides @Singleton Map decoders() { - return ImmutableMap.of("GitHub", jsonDecoder); + @Provides(type = SET) Decoder decoder() { + return new Decoder.TextStream() { + Gson gson = new Gson(); + + @Override public Object decode(Reader reader, Type type) throws IOException { + try { + return gson.fromJson(reader, type); + } catch (JsonIOException e) { + if (e.getCause() != null && e.getCause() instanceof IOException) { + throw IOException.class.cast(e.getCause()); + } + throw e; + } + } + }; } - - final Decoder jsonDecoder = new Decoder() { - Gson gson = new Gson(); - - @Override public Object decode(Reader reader, Type type) { - return gson.fromJson(reader, type); - } - }; } /** @@ -87,16 +88,14 @@ static class GsonModule { */ @Module(overrides = true, library = true) static class JacksonModule { - @Provides @Singleton Map decoders() { - return ImmutableMap.of("GitHub", jsonDecoder); + @Provides(type = SET) Decoder decoder() { + return new Decoder.TextStream() { + ObjectMapper mapper = new ObjectMapper().disable(FAIL_ON_UNKNOWN_PROPERTIES).setVisibility(FIELD, ANY); + + @Override public Object decode(Reader reader, final Type type) throws IOException { + return mapper.readValue(reader, mapper.constructType(type)); + } + }; } - - final Decoder jsonDecoder = new Decoder() { - ObjectMapper mapper = new ObjectMapper().disable(FAIL_ON_UNKNOWN_PROPERTIES).setVisibility(FIELD, ANY); - - @Override public Object decode(Reader reader, final Type type) throws JsonProcessingException, IOException { - return mapper.readValue(reader, mapper.constructType(type)); - } - }; } } diff --git a/feign-core/src/test/java/feign/examples/IAMExample.java b/feign-core/src/test/java/feign/examples/IAMExample.java index fccafbfeef..7f384e2870 100644 --- a/feign-core/src/test/java/feign/examples/IAMExample.java +++ b/feign-core/src/test/java/feign/examples/IAMExample.java @@ -15,12 +15,6 @@ */ package feign.examples; -import com.google.common.collect.ImmutableMap; - -import java.util.Map; - -import javax.inject.Singleton; - import dagger.Module; import dagger.Provides; import feign.Feign; @@ -31,6 +25,8 @@ import feign.codec.Decoder; import feign.codec.Decoders; +import static dagger.Provides.Type.SET; + public class IAMExample { interface IAM { @@ -69,8 +65,8 @@ private IAMTarget(String accessKey, String secretKey) { @Module(overrides = true, library = true) static class IAMModule { - @Provides @Singleton Map decoders() { - return ImmutableMap.of("IAM#arn()", Decoders.firstGroup("([\\S&&[^<]]+)")); + @Provides(type = SET) Decoder decoder() { + return Decoders.firstGroup("([\\S&&[^<]]+)"); } } } diff --git a/feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java b/feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java index 6f02f9f9f3..64621840d8 100644 --- a/feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java +++ b/feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java @@ -18,6 +18,8 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.gson.reflect.TypeToken; +import feign.RequestLine; import org.testng.annotations.Test; import java.lang.annotation.ElementType; @@ -25,6 +27,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.net.URI; +import java.util.List; import javax.ws.rs.DELETE; import javax.ws.rs.FormParam; @@ -169,6 +172,28 @@ interface BodyWithoutParameters { assertEquals(md.template().headers().get(CONTENT_TYPE), ImmutableSet.of(APPLICATION_XML)); } + interface BodyParams { + @POST Response post(List body); + + @POST Response tooMany(List body, List body2); + } + + @Test public void bodyParamIsGeneric() throws Exception { + MethodMetadata md = contract.parseAndValidatateMetadata(BodyParams.class.getDeclaredMethod("post", + List.class)); + assertNull(md.template().body()); + assertNull(md.template().bodyTemplate()); + assertNull(md.urlIndex()); + assertEquals(md.bodyIndex(), Integer.valueOf(0)); + assertEquals(md.bodyType(), new TypeToken>() { + }.getType()); + } + + @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "Method has too many Body.*") + public void tooManyBodies() throws Exception { + contract.parseAndValidatateMetadata(BodyParams.class.getDeclaredMethod("tooMany", List.class, List.class)); + } + interface WithURIParam { @GET @Path("/{1}/{2}") Response uriParam(@PathParam("1") String one, URI endpoint, @PathParam("2") String two); } diff --git a/feign-jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java b/feign-jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java index f8692bf54a..722352ea59 100644 --- a/feign-jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java +++ b/feign-jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java @@ -15,25 +15,24 @@ */ package feign.jaxrs.examples; -import com.google.common.collect.ImmutableMap; import com.google.gson.Gson; - -import java.io.Reader; -import java.lang.reflect.Type; -import java.util.List; -import java.util.Map; - -import javax.inject.Singleton; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; - +import com.google.gson.JsonIOException; import dagger.Module; import dagger.Provides; import feign.Feign; import feign.codec.Decoder; import feign.jaxrs.JAXRSModule; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import java.io.IOException; +import java.io.Reader; +import java.lang.reflect.Type; +import java.util.List; + +import static dagger.Provides.Type.SET; + /** * adapted from {@code com.example.retrofit.GitHubClient} */ @@ -64,16 +63,21 @@ public static void main(String... args) { */ @Module(overrides = true, library = true, includes = JAXRSModule.class) static class GitHubModule { - @Provides @Singleton Map decoders() { - return ImmutableMap.of("GitHub", jsonDecoder); - } + @Provides(type = SET) Decoder decoder() { + return new Decoder.TextStream() { + Gson gson = new Gson(); - final Decoder jsonDecoder = new Decoder() { - Gson gson = new Gson(); - - @Override public Object decode(Reader reader, Type type) { - return gson.fromJson(reader, type); - } - }; + @Override public Object decode(Reader reader, Type type) throws IOException { + try { + return gson.fromJson(reader, type); + } catch (JsonIOException e) { + if (e.getCause() != null && e.getCause() instanceof IOException) { + throw IOException.class.cast(e.getCause()); + } + throw e; + } + } + }; + } } } diff --git a/feign-jaxrs/src/test/java/feign/jaxrs/examples/IAMExample.java b/feign-jaxrs/src/test/java/feign/jaxrs/examples/IAMExample.java index c46c6420db..f303775068 100644 --- a/feign-jaxrs/src/test/java/feign/jaxrs/examples/IAMExample.java +++ b/feign-jaxrs/src/test/java/feign/jaxrs/examples/IAMExample.java @@ -15,11 +15,6 @@ */ package feign.jaxrs.examples; -import com.google.common.collect.ImmutableMap; - -import java.util.Map; - -import javax.inject.Singleton; import javax.ws.rs.GET; import javax.ws.rs.Path; @@ -34,6 +29,8 @@ import feign.examples.AWSSignatureVersion4; import feign.jaxrs.JAXRSModule; +import static dagger.Provides.Type.SET; + public class IAMExample { interface IAM { @@ -72,8 +69,8 @@ private IAMTarget(String accessKey, String secretKey) { @Module(overrides = true, library = true, includes = JAXRSModule.class) static class IAMModule { - @Provides @Singleton Map decoders() { - return ImmutableMap.of("IAM#arn()", Decoders.firstGroup("([\\S&&[^<]]+)")); + @Provides(type = SET) Decoder decoder() { + return Decoders.firstGroup("([\\S&&[^<]]+)"); } } } From 9fd513d268679005d6430337ed8595a08727c1c2 Mon Sep 17 00:00:00 2001 From: adriancole Date: Mon, 15 Jul 2013 14:27:56 -0700 Subject: [PATCH 067/179] remove timestamp from log appender helper --- feign-core/src/main/java/feign/Logger.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/feign-core/src/main/java/feign/Logger.java b/feign-core/src/main/java/feign/Logger.java index 96fdea456c..c9bec2aa55 100644 --- a/feign-core/src/main/java/feign/Logger.java +++ b/feign-core/src/main/java/feign/Logger.java @@ -18,7 +18,6 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.Reader; -import java.text.SimpleDateFormat; import java.util.logging.FileHandler; import java.util.logging.LogRecord; import java.util.logging.SimpleFormatter; @@ -90,18 +89,16 @@ Response logAndRebufferResponse(Target target, Level logLevel, Response respo } /** - * helper that configures jul to sanely log messages. + * helper that configures jul to sanely log messages at FINE level without additional formatting. */ public JavaLogger appendToFile(String logfile) { - final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); logger.setLevel(java.util.logging.Level.FINE); try { FileHandler handler = new FileHandler(logfile, true); handler.setFormatter(new SimpleFormatter() { @Override public String format(LogRecord record) { - String timestamp = sdf.format(new java.util.Date(record.getMillis())); // NOPMD - return String.format("%s %s%n", timestamp, record.getMessage()); // NOPMD + return String.format("%s%n", record.getMessage()); // NOPMD } }); logger.addHandler(handler); From e88f2e530871c3b96a62eb2f39fdd9a65fbcbe8a Mon Sep 17 00:00:00 2001 From: adriancole Date: Sat, 6 Jul 2013 15:52:03 -0700 Subject: [PATCH 068/179] added IncrementalCallback type and updated Contract to process it --- feign-core/src/main/java/feign/Contract.java | 18 +++-- .../main/java/feign/IncrementalCallback.java | 67 +++++++++++++++++++ .../src/main/java/feign/MethodHandler.java | 6 +- .../src/main/java/feign/MethodMetadata.java | 26 +++++-- .../src/main/java/feign/ReflectiveFeign.java | 6 +- .../test/java/feign/DefaultContractTest.java | 42 ++++++++++++ .../main/java/feign/jaxrs/JAXRSModule.java | 17 +++-- .../java/feign/jaxrs/JAXRSContractTest.java | 65 ++++++++++++++---- 8 files changed, 210 insertions(+), 37 deletions(-) create mode 100644 feign-core/src/main/java/feign/IncrementalCallback.java diff --git a/feign-core/src/main/java/feign/Contract.java b/feign-core/src/main/java/feign/Contract.java index 7669fb9549..f9b6d7d29b 100644 --- a/feign-core/src/main/java/feign/Contract.java +++ b/feign-core/src/main/java/feign/Contract.java @@ -15,17 +15,18 @@ */ package feign; +import javax.inject.Named; import java.lang.annotation.Annotation; import java.lang.reflect.Method; +import java.lang.reflect.Type; import java.net.URI; import java.util.ArrayList; import java.util.Collection; import java.util.List; -import javax.inject.Named; - import static feign.Util.checkState; import static feign.Util.emptyToNull; +import static feign.Util.resolveLastTypeParameter; /** * Defines what annotations and values are valid on interfaces. @@ -50,7 +51,7 @@ public List parseAndValidatateMetadata(Class declaring) { */ public MethodMetadata parseAndValidatateMetadata(Method method) { MethodMetadata data = new MethodMetadata(); - data.returnType(method.getGenericReturnType()); + data.decodeInto(method.getGenericReturnType()); data.configKey(Feign.configKey(method)); for (Annotation methodAnnotation : method.getAnnotations()) { @@ -69,8 +70,17 @@ public MethodMetadata parseAndValidatateMetadata(Method method) { } if (parameterTypes[i] == URI.class) { data.urlIndex(i); + } else if (IncrementalCallback.class.isAssignableFrom(parameterTypes[i])) { + checkState(method.getReturnType() == void.class, "IncrementalCallback methods must return void: %s", method); + checkState(i == count - 1, "IncrementalCallback must be the last parameter: %s", method); + Type context = method.getGenericParameterTypes()[i]; + Type incrementalCallbackType = resolveLastTypeParameter(context, IncrementalCallback.class); + data.decodeInto(incrementalCallbackType); + data.incrementalCallbackIndex(i); + checkState(incrementalCallbackType != null, "Expected param %s to be IncrementalCallback or IncrementalCallback or a subtype", + context, incrementalCallbackType); } else if (!isHttpAnnotation) { - checkState(data.formParams().isEmpty(), "Body parameters cannot be used with @FormParam parameters."); + checkState(data.formParams().isEmpty(), "Body parameters cannot be used with form parameters."); checkState(data.bodyIndex() == null, "Method has too many Body parameters: %s", method); data.bodyIndex(i); data.bodyType(method.getGenericParameterTypes()[i]); diff --git a/feign-core/src/main/java/feign/IncrementalCallback.java b/feign-core/src/main/java/feign/IncrementalCallback.java new file mode 100644 index 0000000000..90173be566 --- /dev/null +++ b/feign-core/src/main/java/feign/IncrementalCallback.java @@ -0,0 +1,67 @@ +package feign; + +/** + * Communicates results as they are {@link feign.codec.Decoder decoded} from + * an {@link Response.Body http response body}. {@link #onNext(Object) onNext} + * will be called for each incremental value of type {@code T}, or not at all + * when there are no values present in the response. Methods that accept + * {@code IncrementalCallback} are asynchronous, which implies background + * processing. + *
+ * {@link #onSuccess() onSuccess} or {@link #onFailure(Throwable)} onFailure} + * will be called when the response is finished, but not both. + *
+ * {@code IncrementalCallback} can be used as an asynchronous alternative to a + * {@code Collection}, or any other use where iterative response parsing is + * worth the additional effort to implement this interface. + *
+ *
+ * Here's an example of implementing {@code IncrementalCallback}: + *
+ *
+ * IncrementalCallback counter = new IncrementalCallback() {
+ *
+ *   public int count;
+ *
+ *   @Override public void onNext(Contributor element) {
+ *     count++;
+ *   }
+ *
+ *   @Override public void onSuccess() {
+ *     System.out.println("found " + count + " contributors");
+ *   }
+ *
+ *   @Override public void onFailure(Throwable cause) {
+ *     System.err.println("sad face after contributor " + count);
+ *   }
+ * };
+ * github.contributors("netflix", "feign", counter);
+ * 
+ * + * @param expected value to decode + */ +public interface IncrementalCallback { + /** + * Invoked as soon as new data is available. Could be invoked many times or + * not at all. + * + * @param element next decoded element. + */ + void onNext(T element); + + /** + * Called when response processing completed successfully. + */ + void onSuccess(); + + /** + * Called when response processing failed for any reason. + *
+ * Common failure cases include {@link FeignException}, + * {@link java.io.IOException}, and {@link feign.codec.DecodeException}. + * However, the cause could be a {@code Throwable} of any kind. + * + * @param cause the reason for the failure + */ + void onFailure(Throwable cause); +} diff --git a/feign-core/src/main/java/feign/MethodHandler.java b/feign-core/src/main/java/feign/MethodHandler.java index 0cef816e9d..d686f17981 100644 --- a/feign-core/src/main/java/feign/MethodHandler.java +++ b/feign-core/src/main/java/feign/MethodHandler.java @@ -72,13 +72,13 @@ private SynchronousMethodHandler(Target target, Client client, Provider> indexToName() { } private static final long serialVersionUID = 1L; + } diff --git a/feign-core/src/main/java/feign/ReflectiveFeign.java b/feign-core/src/main/java/feign/ReflectiveFeign.java index 50289e16e8..f6a37eb261 100644 --- a/feign-core/src/main/java/feign/ReflectiveFeign.java +++ b/feign-core/src/main/java/feign/ReflectiveFeign.java @@ -161,13 +161,13 @@ public Map apply(Target key) { List metadata = contract.parseAndValidatateMetadata(key.type()); Map result = new LinkedHashMap(); for (MethodMetadata md : metadata) { - Decoder.TextStream decoder = decoders.get(md.returnType()); + Decoder.TextStream decoder = decoders.get(md.decodeInto()); if (decoder == null) { decoder = decoders.get(Object.class); } if (decoder == null) { throw new IllegalStateException(format("%s needs @Provides(type = Set) Decoder decoder()" + - "{ // Decoder.TextStream<%s> or Decoder.TextStream}", md.configKey(), md.returnType())); + "{ // Decoder.TextStream<%s> or Decoder.TextStream}", md.configKey(), md.decodeInto())); } BuildTemplateByResolvingArgs buildTemplate; if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) { @@ -183,7 +183,7 @@ public Map apply(Target key) { } if (encoder == null) { throw new IllegalStateException(format("%s needs @Provides(type = Set) Encoder encoder()" + - "{ // Encoder.Text<%s> or Encoder.Text}", md.bodyType(), md.returnType())); + "{ // Encoder.Text<%s> or Encoder.Text}", md.bodyType(), md.decodeInto())); } buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder); } else { diff --git a/feign-core/src/test/java/feign/DefaultContractTest.java b/feign-core/src/test/java/feign/DefaultContractTest.java index dc66330073..958a7785fc 100644 --- a/feign-core/src/test/java/feign/DefaultContractTest.java +++ b/feign-core/src/test/java/feign/DefaultContractTest.java @@ -21,6 +21,7 @@ import org.testng.annotations.Test; import javax.inject.Named; +import java.lang.reflect.Type; import java.net.URI; import java.util.List; @@ -237,4 +238,45 @@ interface HeaderParams { assertEquals(md.template().headers().get("Auth-Token"), ImmutableSet.of("{Auth-Token}")); assertEquals(md.indexToName().get(0), ImmutableSet.of("Auth-Token")); } + + interface WithIncrementalCallback { + @RequestLine("GET /") void valid(IncrementalCallback> one); + + @RequestLine("GET /{path}") void badOrder(IncrementalCallback> one, @Named("path") String path); + + @RequestLine("GET /") Response returnType(IncrementalCallback> one); + + @RequestLine("GET /") void wildcardExtends(IncrementalCallback> one); + + @RequestLine("GET /") void subtype(ParameterizedIncrementalCallback> one); + } + + static final List listString = null; + + interface ParameterizedIncrementalCallback> extends IncrementalCallback { + } + + @Test public void methodCanHaveIncrementalCallbackParam() throws Exception { + contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("valid", IncrementalCallback.class)); + } + + @Test public void methodMetadataReturnTypeOnObservableMethodIsItsTypeParameter() throws Exception { + Type listStringType = getClass().getDeclaredField("listString").getGenericType(); + MethodMetadata md = contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("valid", IncrementalCallback.class)); + assertEquals(md.decodeInto(), listStringType); + md = contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("wildcardExtends", IncrementalCallback.class)); + assertEquals(md.decodeInto(), listStringType); + md = contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("subtype", ParameterizedIncrementalCallback.class)); + assertEquals(md.decodeInto(), listStringType); + } + + @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = ".*the last parameter.*") + public void incrementalCallbackParamMustBeLast() throws Exception { + contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("badOrder", IncrementalCallback.class, String.class)); + } + + @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = ".*must return void.*") + public void incrementalCallbackMethodMustReturnVoid() throws Exception { + contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("returnType", IncrementalCallback.class)); + } } diff --git a/feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java b/feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java index 9e766387a1..e9e2a5dba2 100644 --- a/feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java +++ b/feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java @@ -15,9 +15,10 @@ */ package feign.jaxrs; -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; -import java.util.Collection; +import dagger.Provides; +import feign.Body; +import feign.Contract; +import feign.MethodMetadata; import javax.ws.rs.Consumes; import javax.ws.rs.FormParam; @@ -27,11 +28,9 @@ import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; - -import dagger.Provides; -import feign.Body; -import feign.Contract; -import feign.MethodMetadata; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Collection; import static feign.Util.checkState; @@ -44,7 +43,7 @@ public final class JAXRSModule { return new JAXRSContract(); } - static final class JAXRSContract extends Contract { + public static final class JAXRSContract extends Contract { @Override protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) { diff --git a/feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java b/feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java index 64621840d8..36888cad83 100644 --- a/feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java +++ b/feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java @@ -17,18 +17,13 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; - import com.google.gson.reflect.TypeToken; -import feign.RequestLine; +import feign.Body; +import feign.IncrementalCallback; +import feign.MethodMetadata; +import feign.Response; import org.testng.annotations.Test; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import java.net.URI; -import java.util.List; - import javax.ws.rs.DELETE; import javax.ws.rs.FormParam; import javax.ws.rs.GET; @@ -40,10 +35,13 @@ import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; - -import feign.Body; -import feign.MethodMetadata; -import feign.Response; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Type; +import java.net.URI; +import java.util.List; import static feign.jaxrs.JAXRSModule.CONTENT_TYPE; import static javax.ws.rs.HttpMethod.DELETE; @@ -264,4 +262,45 @@ interface HeaderParams { assertEquals(md.template().headers().get("Auth-Token"), ImmutableSet.of("{Auth-Token}")); assertEquals(md.indexToName().get(0), ImmutableSet.of("Auth-Token")); } + + interface WithIncrementalCallback { + @GET @Path("/") void valid(IncrementalCallback> one); + + @GET @Path("/{path}") void badOrder(IncrementalCallback> one, @PathParam("path") String path); + + @GET @Path("/") Response returnType(IncrementalCallback> one); + + @GET @Path("/") void wildcardExtends(IncrementalCallback> one); + + @GET @Path("/") void subtype(ParameterizedIncrementalCallback> one); + } + + static final List listString = null; + + interface ParameterizedIncrementalCallback> extends IncrementalCallback { + } + + @Test public void methodCanHaveIncrementalCallbackParam() throws Exception { + contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("valid", IncrementalCallback.class)); + } + + @Test public void methodMetadataReturnTypeOnObservableMethodIsItsTypeParameter() throws Exception { + Type listStringType = getClass().getDeclaredField("listString").getGenericType(); + MethodMetadata md = contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("valid", IncrementalCallback.class)); + assertEquals(md.decodeInto(), listStringType); + md = contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("wildcardExtends", IncrementalCallback.class)); + assertEquals(md.decodeInto(), listStringType); + md = contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("subtype", ParameterizedIncrementalCallback.class)); + assertEquals(md.decodeInto(), listStringType); + } + + @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = ".*the last parameter.*") + public void incrementalCallbackParamMustBeLast() throws Exception { + contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("badOrder", IncrementalCallback.class, String.class)); + } + + @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = ".*must return void.*") + public void incrementalCallbackMethodMustReturnVoid() throws Exception { + contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("returnType", IncrementalCallback.class)); + } } From 309d4b3414d47b6a8a9140a15149344bd24eff03 Mon Sep 17 00:00:00 2001 From: adriancole Date: Thu, 11 Jul 2013 20:46:33 -0700 Subject: [PATCH 069/179] integrated IncrementalCallback into methodhandler and added IncrementalDecoder --- feign-core/src/main/java/feign/Feign.java | 45 +++++- .../src/main/java/feign/MethodHandler.java | 98 +++++++++++- .../src/main/java/feign/ReflectiveFeign.java | 53 +++++-- .../src/main/java/feign/codec/Decoder.java | 23 +-- .../main/java/feign/codec/ErrorDecoder.java | 10 ++ .../java/feign/codec/IncrementalDecoder.java | 114 ++++++++++++++ .../feign/codec/StringIncrementalDecoder.java | 32 ++++ feign-core/src/test/java/feign/FeignTest.java | 146 +++++++++++++++++- 8 files changed, 481 insertions(+), 40 deletions(-) create mode 100644 feign-core/src/main/java/feign/codec/IncrementalDecoder.java create mode 100644 feign-core/src/main/java/feign/codec/StringIncrementalDecoder.java diff --git a/feign-core/src/main/java/feign/Feign.java b/feign-core/src/main/java/feign/Feign.java index b5de31cf68..171116f4dc 100644 --- a/feign-core/src/main/java/feign/Feign.java +++ b/feign-core/src/main/java/feign/Feign.java @@ -15,6 +15,8 @@ */ package feign; + +import dagger.Lazy; import dagger.ObjectGraph; import dagger.Provides; import feign.Logger.NoOpLogger; @@ -23,13 +25,23 @@ import feign.codec.Decoder; import feign.codec.Encoder; import feign.codec.ErrorDecoder; +import feign.codec.IncrementalDecoder; +import javax.inject.Named; +import javax.inject.Singleton; import javax.net.ssl.SSLSocketFactory; +import java.io.Closeable; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; + +import static java.lang.Thread.MIN_PRIORITY; /** * Feign's purpose is to ease development against http apis that feign @@ -38,7 +50,7 @@ * In implementation, Feign is a {@link Feign#newInstance factory} for * generating {@link Target targeted} http apis. */ -public abstract class Feign { +public abstract class Feign implements Closeable { /** * Returns a new instance of an HTTP API, defined by annotations in the @@ -119,6 +131,26 @@ public static class Defaults { @Provides Set noDecoders() { return Collections.emptySet(); } + + @Provides Set noIncrementalDecoders() { + return Collections.emptySet(); + } + + /** + * Used for both http invocation and decoding when incrementalCallbacks are used. + */ + @Provides @Singleton @Named("http") Executor httpExecutor() { + return Executors.newCachedThreadPool(new ThreadFactory() { + @Override public Thread newThread(final Runnable r) { + return new Thread(new Runnable() { + @Override public void run() { + Thread.currentThread().setPriority(MIN_PRIORITY); + r.run(); + } + }, MethodHandler.IDLE_THREAD_NAME); + } + }); + } } /** @@ -162,7 +194,16 @@ private static List modulesForGraph(Object... modules) { return modulesForGraph; } - Feign() { + private final Lazy httpExecutor; + Feign(Lazy httpExecutor) { + this.httpExecutor = httpExecutor; + } + + @Override public void close() { + Executor e = httpExecutor.get(); + if (e instanceof ExecutorService) { + ExecutorService.class.cast(e).shutdownNow(); + } } } diff --git a/feign-core/src/main/java/feign/MethodHandler.java b/feign-core/src/main/java/feign/MethodHandler.java index d686f17981..42076ff5af 100644 --- a/feign-core/src/main/java/feign/MethodHandler.java +++ b/feign-core/src/main/java/feign/MethodHandler.java @@ -15,14 +15,19 @@ */ package feign; +import dagger.Lazy; import feign.Request.Options; import feign.codec.DecodeException; import feign.codec.Decoder; import feign.codec.ErrorDecoder; +import feign.codec.IncrementalDecoder; import javax.inject.Inject; +import javax.inject.Named; import javax.inject.Provider; import java.io.IOException; +import java.io.Reader; +import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import static feign.FeignException.errorExecuting; @@ -32,6 +37,12 @@ abstract class MethodHandler { + /** + * same approach as retrofit: temporarily rename threads + */ + static final String THREAD_PREFIX = "Feign-"; + static final String IDLE_THREAD_NAME = THREAD_PREFIX + "Idle"; + /** * Those using guava will implement as {@code Function}. */ @@ -42,12 +53,15 @@ static interface BuildTemplateFromArgs { static class Factory { private final Client client; + private final Lazy httpExecutor; private final Provider retryer; private final Logger logger; private final Logger.Level logLevel; - @Inject Factory(Client client, Provider retryer, Logger logger, Logger.Level logLevel) { + @Inject Factory(Client client, @Named("http") Lazy httpExecutor, Provider retryer, Logger logger, + Logger.Level logLevel) { this.client = checkNotNull(client, "client"); + this.httpExecutor = checkNotNull(httpExecutor, "httpExecutor"); this.retryer = checkNotNull(retryer, "retryer"); this.logger = checkNotNull(logger, "logger"); this.logLevel = checkNotNull(logLevel, "logLevel"); @@ -58,6 +72,78 @@ public MethodHandler create(Target target, MethodMetadata md, BuildTemplateFr return new SynchronousMethodHandler(target, client, retryer, logger, logLevel, md, buildTemplateFromArgs, options, decoder, errorDecoder); } + + public MethodHandler create(Target target, MethodMetadata md, BuildTemplateFromArgs buildTemplateFromArgs, + Options options, IncrementalDecoder.TextStream incrementalCallbackDecoder, + ErrorDecoder errorDecoder) { + return new IncrementalCallbackMethodHandler(target, client, retryer, logger, logLevel, md, buildTemplateFromArgs, + options, incrementalCallbackDecoder, errorDecoder, httpExecutor); + } + } + + static final class IncrementalCallbackMethodHandler extends MethodHandler { + private final Lazy httpExecutor; + private final IncrementalDecoder.TextStream incDecoder; + + private IncrementalCallbackMethodHandler(Target target, Client client, Provider retryer, Logger logger, + Logger.Level logLevel, MethodMetadata metadata, + BuildTemplateFromArgs buildTemplateFromArgs, Options options, + IncrementalDecoder.TextStream incDecoder, ErrorDecoder errorDecoder, + Lazy httpExecutor) { + super(target, client, retryer, logger, logLevel, metadata, buildTemplateFromArgs, options, errorDecoder); + this.httpExecutor = checkNotNull(httpExecutor, "httpExecutor for %s", target); + this.incDecoder = checkNotNull(incDecoder, "incrementalCallbackDecoder for %s", target); + } + + @Override public Object invoke(final Object[] argv) throws Throwable { + httpExecutor.get().execute(new Runnable() { + @Override public void run() { + Error error = null; + Object arg = argv[metadata.incrementalCallbackIndex()]; + IncrementalCallback incrementalCallback = IncrementalCallback.class.cast(arg); + try { + IncrementalCallbackMethodHandler.super.invoke(argv); + incrementalCallback.onSuccess(); + } catch (Error cause) { + // assign to a variable in case .onFailure throws a RTE + error = cause; + incrementalCallback.onFailure(cause); + } catch (Throwable cause) { + incrementalCallback.onFailure(cause); + } finally { + Thread.currentThread().setName(IDLE_THREAD_NAME); + if (error != null) + throw error; + } + } + }); + return null; // void. + } + + @Override protected Object decode(Object[] argv, Response response) throws Throwable { + Object arg = argv[metadata.incrementalCallbackIndex()]; + IncrementalCallback incrementalCallback = IncrementalCallback.class.cast(arg); + if (metadata.decodeInto().equals(Response.class)) { + incrementalCallback.onNext(response); + } else if (metadata.decodeInto() != Void.class) { + Response.Body body = response.body(); + if (body == null) + return null; + Reader reader = body.asReader(); + try { + incDecoder.decode(reader, metadata.decodeInto(), incrementalCallback); + } finally { + ensureClosed(body); + } + } + return null; // void + } + + @Override protected Request targetRequest(RequestTemplate template) { + Request request = super.targetRequest(template); + Thread.currentThread().setName(THREAD_PREFIX + metadata.configKey()); + return request; + } } static final class SynchronousMethodHandler extends MethodHandler { @@ -125,10 +211,8 @@ public Object invoke(Object[] argv) throws Throwable { } } - public Object executeAndDecode(Object[] argv, RequestTemplate template) - throws Throwable { - // create the request from a mutable copy of the input template. - Request request = target.apply(new RequestTemplate(template)); + 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); @@ -159,5 +243,9 @@ public Object executeAndDecode(Object[] argv, RequestTemplate template) } } + protected Request targetRequest(RequestTemplate template) { + return target.apply(new RequestTemplate(template)); + } + protected abstract Object decode(Object[] argv, Response response) throws Throwable; } diff --git a/feign-core/src/main/java/feign/ReflectiveFeign.java b/feign-core/src/main/java/feign/ReflectiveFeign.java index f6a37eb261..d4e227dc74 100644 --- a/feign-core/src/main/java/feign/ReflectiveFeign.java +++ b/feign-core/src/main/java/feign/ReflectiveFeign.java @@ -15,15 +15,19 @@ */ package feign; +import dagger.Lazy; import dagger.Provides; import feign.Request.Options; import feign.codec.Decoder; import feign.codec.EncodeException; import feign.codec.Encoder; import feign.codec.ErrorDecoder; +import feign.codec.IncrementalDecoder; import feign.codec.StringDecoder; +import feign.codec.StringIncrementalDecoder; import javax.inject.Inject; +import javax.inject.Named; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; @@ -35,6 +39,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.concurrent.Executor; import static feign.Util.checkArgument; import static feign.Util.checkNotNull; @@ -47,7 +52,8 @@ public class ReflectiveFeign extends Feign { private final ParseHandlersByName targetToHandlersByName; - @Inject ReflectiveFeign(ParseHandlersByName targetToHandlersByName) { + @Inject ReflectiveFeign(@Named("http") Lazy httpExecutor, ParseHandlersByName targetToHandlersByName) { + super(httpExecutor); this.targetToHandlersByName = targetToHandlersByName; } @@ -118,12 +124,15 @@ static final class ParseHandlersByName { private final Map> encoders = new HashMap>(); private final Encoder.Text> formEncoder; private final Map> decoders = new HashMap>(); + private final Map> incrementalDecoders = + new HashMap>(); private final ErrorDecoder errorDecoder; private final MethodHandler.Factory factory; @SuppressWarnings("unchecked") @Inject ParseHandlersByName(Contract contract, Options options, Set encoders, Set decoders, - ErrorDecoder errorDecoder, MethodHandler.Factory factory) { + Set incrementalDecoders, ErrorDecoder errorDecoder, + MethodHandler.Factory factory) { this.contract = contract; this.options = options; this.factory = factory; @@ -155,20 +164,22 @@ static final class ParseHandlersByName { Type type = resolveLastTypeParameter(decoder.getClass(), Decoder.class); this.decoders.put(type, Decoder.TextStream.class.cast(decoder)); } + StringIncrementalDecoder stringIncrementalDecoder = new StringIncrementalDecoder(); + this.incrementalDecoders.put(Void.class, stringIncrementalDecoder); + this.incrementalDecoders.put(Response.class, stringIncrementalDecoder); + this.incrementalDecoders.put(String.class, stringIncrementalDecoder); + for (IncrementalDecoder incrementalDecoder : incrementalDecoders) { + checkState(incrementalDecoder instanceof IncrementalDecoder.TextStream, + "Currently, only IncrementalDecoder.TextStream is supported. Found: ", incrementalDecoder); + Type type = resolveLastTypeParameter(incrementalDecoder.getClass(), IncrementalDecoder.class); + this.incrementalDecoders.put(type, IncrementalDecoder.TextStream.class.cast(incrementalDecoder)); + } } public Map apply(Target key) { List metadata = contract.parseAndValidatateMetadata(key.type()); Map result = new LinkedHashMap(); for (MethodMetadata md : metadata) { - Decoder.TextStream decoder = decoders.get(md.decodeInto()); - if (decoder == null) { - decoder = decoders.get(Object.class); - } - if (decoder == null) { - throw new IllegalStateException(format("%s needs @Provides(type = Set) Decoder decoder()" + - "{ // Decoder.TextStream<%s> or Decoder.TextStream}", md.configKey(), md.decodeInto())); - } BuildTemplateByResolvingArgs buildTemplate; if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) { if (formEncoder == null) { @@ -189,7 +200,27 @@ public Map apply(Target key) { } else { buildTemplate = new BuildTemplateByResolvingArgs(md); } - result.put(md.configKey(), factory.create(key, md, buildTemplate, options, decoder, errorDecoder)); + if (md.incrementalCallbackIndex() != null) { + IncrementalDecoder.TextStream incrementalDecoder = incrementalDecoders.get(md.decodeInto()); + if (incrementalDecoder == null) { + incrementalDecoder = incrementalDecoders.get(Object.class); + } + if (incrementalDecoder == null) { + throw new IllegalStateException(format("%s needs @Provides(type = Set) IncrementalDecoder incrementalDecoder()" + + "{ // IncrementalDecoder.TextStream<%s> or IncrementalDecoder.TextStream}", md.configKey(), md.decodeInto())); + } + result.put(md.configKey(), factory.create(key, md, buildTemplate, options, incrementalDecoder, errorDecoder)); + } else { + Decoder.TextStream decoder = decoders.get(md.decodeInto()); + if (decoder == null) { + decoder = decoders.get(Object.class); + } + if (decoder == null) { + throw new IllegalStateException(format("%s needs @Provides(type = Set) Decoder decoder()" + + "{ // Decoder.TextStream<%s> or Decoder.TextStream}", md.configKey(), md.decodeInto())); + } + result.put(md.configKey(), factory.create(key, md, buildTemplate, options, decoder, errorDecoder)); + } } return result; } diff --git a/feign-core/src/main/java/feign/codec/Decoder.java b/feign-core/src/main/java/feign/codec/Decoder.java index afcc6406ee..8492d143b4 100644 --- a/feign-core/src/main/java/feign/codec/Decoder.java +++ b/feign-core/src/main/java/feign/codec/Decoder.java @@ -15,41 +15,30 @@ */ package feign.codec; +import feign.FeignException; +import feign.Response; + import java.io.IOException; import java.io.Reader; import java.lang.reflect.Type; -import feign.FeignException; -import feign.Response; - /** * Decodes an HTTP response into a given type. Invoked when * {@link Response#status()} is in the 2xx range. Like * {@code javax.websocket.Decoder}, except that the decode method is passed the * generic type of the target.
- *
- *
- * Error handling
- *
- * Responses where {@link Response#status()} is not in the 2xx range are - * classified as errors, addressed by the {@link ErrorDecoder}. That said, - * certain RPC apis return errors defined in the {@link Response#body()} even on - * a 200 status. For example, in the DynECT api, a job still running condition - * is returned with a 200 status, encoded in json. When scenarios like this - * occur, you should raise an application-specific exception (which may be - * {@link feign.RetryableException retryable}). * * @param input that can be derived from {@link feign.Response.Body}. * @param widest type an instance of this can decode. */ public interface Decoder { /** - * Implement this to decode a resource to an object of the specified type. + * Implement this to decode a resource to an object into a single object. * If you need to wrap exceptions, please do so via {@link DecodeException}. * * @param input if {@code Closeable}, no need to close this, as the caller - * manages resources. - * @param type Target object type. + * manages resources. + * @param type Target object type. * @return instance of {@code type} * @throws IOException will be propagated safely to the caller. * @throws DecodeException when decoding failed due to a checked exception diff --git a/feign-core/src/main/java/feign/codec/ErrorDecoder.java b/feign-core/src/main/java/feign/codec/ErrorDecoder.java index d9982360e5..273202d400 100644 --- a/feign-core/src/main/java/feign/codec/ErrorDecoder.java +++ b/feign-core/src/main/java/feign/codec/ErrorDecoder.java @@ -51,6 +51,16 @@ * * } * + *
+ * Error handling
+ *
+ * Responses where {@link Response#status()} is not in the 2xx range are + * classified as errors, addressed by the {@link ErrorDecoder}. That said, + * certain RPC apis return errors defined in the {@link Response#body()} even on + * a 200 status. For example, in the DynECT api, a job still running condition + * is returned with a 200 status, encoded in json. When scenarios like this + * occur, you should raise an application-specific exception (which may be + * {@link feign.RetryableException retryable}). */ public interface ErrorDecoder { diff --git a/feign-core/src/main/java/feign/codec/IncrementalDecoder.java b/feign-core/src/main/java/feign/codec/IncrementalDecoder.java new file mode 100644 index 0000000000..30f27a04bf --- /dev/null +++ b/feign-core/src/main/java/feign/codec/IncrementalDecoder.java @@ -0,0 +1,114 @@ +/* + * 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.codec; + +import feign.FeignException; +import feign.IncrementalCallback; + +import java.io.IOException; +import java.io.Reader; +import java.lang.reflect.Type; + +/** + * Decodes an HTTP response incrementally into an {@link IncrementalCallback} + * via a series of {@link IncrementalCallback#onNext(Object) onNext} calls. + *

+ * Invoked when {@link feign.Response#status()} is in the 2xx range. + * + * @param input that can be derived from {@link feign.Response.Body}. + * @param widest type an instance of this can decode. + */ +public interface IncrementalDecoder { + /** + * Implement this to decode a resource to an object into a single object. + * If you need to wrap exceptions, please do so via {@link feign.codec.DecodeException}. + *
+ * Do not call {@link feign.IncrementalCallback#onSuccess() onSuccess} or + * {@link feign.IncrementalCallback#onFailure onFailure}. + * + * @param input if {@code Closeable}, no need to close this, as the caller + * manages resources. + * @param type type parameter of {@link feign.IncrementalCallback#onNext}. + * @param incrementalCallback call {@link feign.IncrementalCallback#onNext onNext} + * each time an object of {@code type} is decoded + * from the response. + * @throws java.io.IOException will be propagated safely to the caller. + * @throws feign.codec.DecodeException when decoding failed due to a checked exception + * besides IOException. + * @throws feign.FeignException when decoding succeeds, but conveys the operation + * failed. + */ + void decode(I input, Type type, IncrementalCallback incrementalCallback) + throws IOException, DecodeException, FeignException; + + /** + * Used for text-based apis, follows + * {@link feign.codec.IncrementalDecoder#decode(Object, java.lang.reflect.Type, IncrementalCallback)} + * semantics, applied to inputs of type {@link java.io.Reader}.
+ * Ex.
+ *

+ *

+   * public class GsonDecoder implements Decoder.TextStream<Object> {
+   *   private final Gson gson;
+   *
+   *   public GsonDecoder(Gson gson) {
+   *     this.gson = gson;
+   *   }
+   *
+   *   @Override
+   *   public Object decode(Reader reader, Type type) throws IOException {
+   *     try {
+   *       return gson.fromJson(reader, type);
+   *     } catch (JsonIOException e) {
+   *       if (e.getCause() != null &&
+   *           e.getCause() instanceof IOException) {
+   *         throw IOException.class.cast(e.getCause());
+   *       }
+   *       throw e;
+   *     }
+   *   }
+   * }
+   * 
+ *
+   * public class GsonIncrementalDecoder implements IncrementalDecoder {
+   *   private final Gson gson;
+   *
+   *   public GsonIncrementalDecoder(Gson gson) {
+   *     this.gson = gson;
+   *   }
+   *
+   *   @Override public void decode(Reader reader, Type type, IncrementalCallback incrementalCallback) throws Exception {
+   *     JsonReader jsonReader = new JsonReader(reader);
+   *     jsonReader.beginArray();
+   *     while (jsonReader.hasNext()) {
+   *       try {
+   *          incrementalCallback.onNext(gson.fromJson(jsonReader, type));
+   *       } catch (JsonIOException e) {
+   *         if (e.getCause() != null &&
+   *             e.getCause() instanceof IOException) {
+   *           throw IOException.class.cast(e.getCause());
+   *         }
+   *         throw e;
+   *       }
+   *     }
+   *     jsonReader.endArray();
+   *   }
+   * }
+   * 
+   */
+  public interface TextStream extends IncrementalDecoder {
+  }
+}
diff --git a/feign-core/src/main/java/feign/codec/StringIncrementalDecoder.java b/feign-core/src/main/java/feign/codec/StringIncrementalDecoder.java
new file mode 100644
index 0000000000..3e9dc8e005
--- /dev/null
+++ b/feign-core/src/main/java/feign/codec/StringIncrementalDecoder.java
@@ -0,0 +1,32 @@
+/*
+ * 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.codec;
+
+import feign.IncrementalCallback;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.lang.reflect.Type;
+
+public class StringIncrementalDecoder implements IncrementalDecoder.TextStream {
+  private static final StringDecoder STRING_DECODER = new StringDecoder();
+
+  @Override
+  public void decode(Reader reader, Type type, IncrementalCallback incrementalCallback)
+      throws IOException {
+    incrementalCallback.onNext(STRING_DECODER.decode(reader, type));
+  }
+}
diff --git a/feign-core/src/test/java/feign/FeignTest.java b/feign-core/src/test/java/feign/FeignTest.java
index 3155e1549f..ac91708ba7 100644
--- a/feign-core/src/test/java/feign/FeignTest.java
+++ b/feign-core/src/test/java/feign/FeignTest.java
@@ -19,10 +19,10 @@
 import com.google.mockwebserver.MockResponse;
 import com.google.mockwebserver.MockWebServer;
 import com.google.mockwebserver.SocketPolicy;
+import dagger.Lazy;
 import dagger.Module;
 import dagger.Provides;
 import feign.codec.Decoder;
-import feign.codec.EncodeException;
 import feign.codec.Encoder;
 import feign.codec.ErrorDecoder;
 import feign.codec.StringDecoder;
@@ -38,14 +38,35 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 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
 // unbound wildcards are not currently injectable in dagger.
 @SuppressWarnings("rawtypes")
 public class FeignTest {
+
+  @Test public void closeShutsdownExecutorService() throws IOException, InterruptedException {
+    final ExecutorService service = Executors.newCachedThreadPool();
+    new Feign(new Lazy() {
+      @Override public Executor get() {
+        return service;
+      }
+    }) {
+      @Override public  T newInstance(Target target) {
+        return null;
+      }
+    }.close();
+    assertTrue(service.isShutdown());
+  }
+
   interface TestInterface {
     @RequestLine("POST /") String post();
 
@@ -54,15 +75,19 @@ interface TestInterface {
     void login(
         @Named("customer_name") String customer, @Named("user_name") String user, @Named("password") String password);
 
-    @RequestLine("POST /")
-    void body(List contents);
+    @RequestLine("POST /") void body(List contents);
 
-    @RequestLine("POST /")
-    void form(
+    @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("POST /") void incrementVoid(IncrementalCallback incrementalCallback);
+
+    @RequestLine("POST /") void incrementString(IncrementalCallback incrementalCallback);
+
+    @RequestLine("POST /") void incrementResponse(IncrementalCallback incrementalCallback);
+
     @dagger.Module(overrides = true, library = true)
     static class Module {
       @Provides(type = SET) Encoder defaultEncoder() {
@@ -80,6 +105,117 @@ static class Module {
           }
         };
       }
+
+      // just run synchronously
+      @Provides @Singleton @Named("http") Executor httpExecutor() {
+        return new Executor() {
+          @Override public void execute(Runnable command) {
+            command.run();
+          }
+        };
+      }
+    }
+  }
+
+  @Test
+  public void incrementVoid() throws IOException, InterruptedException {
+    final MockWebServer server = new MockWebServer();
+    server.enqueue(new MockResponse().setResponseCode(200).setBody("foo"));
+    server.play();
+
+    try {
+      TestInterface api = Feign.create(TestInterface.class, server.getUrl("").toString(), new TestInterface.Module());
+
+      final AtomicBoolean success = new AtomicBoolean();
+
+      IncrementalCallback incrementalCallback = new IncrementalCallback() {
+
+        @Override public void onNext(Void element) {
+          fail("on next isn't valid for void");
+        }
+
+        @Override public void onSuccess() {
+          success.set(true);
+        }
+
+        @Override public void onFailure(Throwable cause) {
+          fail(cause.getMessage());
+        }
+      };
+      api.incrementVoid(incrementalCallback);
+
+      assertTrue(success.get());
+      assertEquals(server.getRequestCount(), 1);
+    } finally {
+      server.shutdown();
+    }
+  }
+
+  @Test
+  public void incrementResponse() throws IOException, InterruptedException {
+    final MockWebServer server = new MockWebServer();
+    server.enqueue(new MockResponse().setResponseCode(200).setBody("foo"));
+    server.play();
+
+    try {
+      TestInterface api = Feign.create(TestInterface.class, server.getUrl("").toString(), new TestInterface.Module());
+
+      final AtomicBoolean success = new AtomicBoolean();
+
+      IncrementalCallback incrementalCallback = new IncrementalCallback() {
+
+        @Override public void onNext(Response element) {
+          assertEquals(element.status(), 200);
+        }
+
+        @Override public void onSuccess() {
+          success.set(true);
+        }
+
+        @Override public void onFailure(Throwable cause) {
+          fail(cause.getMessage());
+        }
+      };
+      api.incrementResponse(incrementalCallback);
+
+      assertTrue(success.get());
+      assertEquals(server.getRequestCount(), 1);
+    } finally {
+      server.shutdown();
+    }
+  }
+
+  @Test
+  public void incrementString() throws IOException, InterruptedException {
+    final MockWebServer server = new MockWebServer();
+    server.enqueue(new MockResponse().setResponseCode(200).setBody("foo"));
+    server.play();
+
+    try {
+      TestInterface api = Feign.create(TestInterface.class, server.getUrl("").toString(), new TestInterface.Module());
+
+      final AtomicBoolean success = new AtomicBoolean();
+
+      IncrementalCallback incrementalCallback = new IncrementalCallback() {
+
+        @Override public void onNext(String element) {
+          assertEquals(element, "foo");
+        }
+
+        @Override public void onSuccess() {
+          success.set(true);
+        }
+
+        @Override public void onFailure(Throwable cause) {
+          fail(cause.getMessage());
+        }
+      };
+      api.incrementString(incrementalCallback);
+
+      assertTrue(success.get());
+      assertEquals(server.getRequestCount(), 1);
+    } finally {
+      server.shutdown();
     }
   }
 

From 0d7a69b81e98d8b4ffff168facdf60ee0d2825f8 Mon Sep 17 00:00:00 2001
From: adriancole 
Date: Thu, 11 Jul 2013 20:49:23 -0700
Subject: [PATCH 070/179] added IncrementalCallback example code and updated
 changelog

---
 CHANGES.md                                    |   1 +
 README.md                                     |  53 ++++++++
 .../java/feign/examples/GitHubExample.java    | 114 +++++++++++++-----
 3 files changed, 136 insertions(+), 32 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index bf2eb4e20d..8c6102d972 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,4 +1,5 @@
 ### Version 3.0
+* Added support for asynchronous callbacks via `IncrementalCallback` and `IncrementalDecoder.TextStream`.
 * Wire is now Logger, with configurable Logger.Level.
 * changed codec to be similar to [WebSocket JSR 356](http://docs.oracle.com/javaee/7/api/javax/websocket/package-summary.html)
   * Decoder is now `Decoder.TextStream`
diff --git a/README.md b/README.md
index b60ba16e4c..95195ce31d 100644
--- a/README.md
+++ b/README.md
@@ -68,6 +68,59 @@ The generic parameter of `Decoder.TextStream` designates which The type param
   return new SAXDecoder(handlers){};
 }
 ```
+### Asynchronous Incremental Callbacks
+If specified as the last argument of a method `IncrementalCallback` fires a background task to add new elements to the callback as they are decoded.  Think of `IncrementalCallback` as an asynchronous equivalent to a lazy sequence.
+
+Here's how one looks:
+```java
+IncrementalCallback printlnObserver = new IncrementalCallback() {
+
+  public int count;
+
+  @Override public void onNext(Contributor element) {
+    count++;
+  }
+
+  @Override public void onSuccess() {
+    System.out.println("found " + count + " contributors");
+  }
+
+  @Override public void onFailure(Throwable cause) {
+    cause.printStackTrace();
+  }
+};
+github.contributors("netflix", "feign", printlnObserver);
+```
+#### Incremental Decoding
+When using an `IncrementalCallback`, you'll need to configure an `IncrementalDecoderi.TextStream` or a general one for all types (`IncrementalDecoder.TextStream`).
+
+Here's how to wire in a reflective incremental json decoder:
+```java
+@Provides(type = SET) IncrementalDecoder incrementalDecoder(final Gson gson) {
+  return new IncrementalDecoder.TextStream() {
+
+    @Override
+    public void decode(Reader reader, Type type, IncrementalCallback incrementalCallback) throws IOException {
+      JsonReader jsonReader = new JsonReader(reader);
+      jsonReader.beginArray();
+      while (jsonReader.hasNext()) {
+        try {
+          incrementalCallback.onNext(gson.fromJson(jsonReader, type));
+        } catch (JsonIOException e) {
+          if (e.getCause() != null && e.getCause() instanceof IOException) {
+            throw IOException.class.cast(e.getCause());
+          }
+          throw e;
+        }
+      }
+      jsonReader.endArray();
+    }
+  };
+}
+```
+
+
+
 ### Multiple Interfaces
 Feign can produce multiple api interfaces.  These are defined as `Target` (default `HardCodedTarget`), which allow for dynamic discovery and decoration of requests prior to execution.
 
diff --git a/feign-core/src/test/java/feign/examples/GitHubExample.java b/feign-core/src/test/java/feign/examples/GitHubExample.java
index 502f0f3e22..5ecc8cb1d4 100644
--- a/feign-core/src/test/java/feign/examples/GitHubExample.java
+++ b/feign-core/src/test/java/feign/examples/GitHubExample.java
@@ -15,24 +15,26 @@
  */
 package feign.examples;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.gson.Gson;
 import com.google.gson.JsonIOException;
+import com.google.gson.stream.JsonReader;
 import dagger.Module;
 import dagger.Provides;
 import feign.Feign;
+import feign.IncrementalCallback;
 import feign.RequestLine;
 import feign.codec.Decoder;
+import feign.codec.IncrementalDecoder;
 
+import javax.inject.Inject;
 import javax.inject.Named;
+import javax.inject.Singleton;
 import java.io.IOException;
 import java.io.Reader;
 import java.lang.reflect.Type;
 import java.util.List;
+import java.util.concurrent.CountDownLatch;
 
-import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY;
-import static com.fasterxml.jackson.annotation.PropertyAccessor.FIELD;
-import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
 import static dagger.Provides.Type.SET;
 
 /**
@@ -43,6 +45,10 @@ public class GitHubExample {
   interface GitHub {
     @RequestLine("GET /repos/{owner}/{repo}/contributors")
     List 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 contributors);
   }
 
   static class Contributor {
@@ -50,14 +56,46 @@ static class Contributor {
     int contributions;
   }
 
-  public static void main(String... args) {
+  public static void main(String... args) throws InterruptedException {
     GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule());
 
-    // Fetch and print a list of the contributors to this library.
+    System.out.println("Let's fetch and print a list of the contributors to this library.");
     List 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 task = new IncrementalCallback() {
+
+      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);
   }
 
   /**
@@ -65,37 +103,49 @@ public static void main(String... args) {
    */
   @Module(overrides = true, library = true)
   static class GsonModule {
-    @Provides(type = SET) Decoder decoder() {
-      return new Decoder.TextStream() {
-        Gson gson = new Gson();
-
-        @Override public Object decode(Reader reader, Type type) throws IOException {
-          try {
-            return gson.fromJson(reader, type);
-          } catch (JsonIOException e) {
-            if (e.getCause() != null && e.getCause() instanceof IOException) {
-              throw IOException.class.cast(e.getCause());
-            }
-            throw e;
-          }
-        }
-      };
+    @Provides @Singleton Gson gson() {
+      return new Gson();
+    }
+
+    @Provides(type = SET) Decoder decoder(GsonDecoder gsonDecoder) {
+      return gsonDecoder;
+    }
+
+    @Provides(type = SET) IncrementalDecoder incrementalDecoder(GsonDecoder gsonDecoder) {
+      return gsonDecoder;
     }
   }
 
-  /**
-   * Here's how to wire jackson deserialization.
-   */
-  @Module(overrides = true, library = true)
-  static class JacksonModule {
-    @Provides(type = SET) Decoder decoder() {
-      return new Decoder.TextStream() {
-        ObjectMapper mapper = new ObjectMapper().disable(FAIL_ON_UNKNOWN_PROPERTIES).setVisibility(FIELD, ANY);
+  static class GsonDecoder implements Decoder.TextStream, IncrementalDecoder.TextStream {
+    private final Gson gson;
+
+    @Inject GsonDecoder(Gson gson) {
+      this.gson = gson;
+    }
+
+    @Override public Object decode(Reader reader, Type type) throws IOException {
+      return fromJson(new JsonReader(reader), type);
+    }
+
+    @Override
+    public void decode(Reader reader, Type type, IncrementalCallback incrementalCallback) throws IOException {
+      JsonReader jsonReader = new JsonReader(reader);
+      jsonReader.beginArray();
+      while (jsonReader.hasNext()) {
+        incrementalCallback.onNext(fromJson(jsonReader, type));
+      }
+      jsonReader.endArray();
+    }
 
-        @Override public Object decode(Reader reader, final Type type) throws IOException {
-          return mapper.readValue(reader, mapper.constructType(type));
+    private Object fromJson(JsonReader jsonReader, Type type) throws IOException {
+      try {
+        return gson.fromJson(jsonReader, type);
+      } catch (JsonIOException e) {
+        if (e.getCause() != null && e.getCause() instanceof IOException) {
+          throw IOException.class.cast(e.getCause());
         }
-      };
+        throw e;
+      }
     }
   }
 }

From 9fb1c0719d73eaa267f7b51e7db677d2936536ea Mon Sep 17 00:00:00 2001
From: adriancole 
Date: Mon, 15 Jul 2013 18:51:34 -0700
Subject: [PATCH 071/179] Added feign-gson codec, used via new GsonModule()

---
 CHANGES.md                                    |   1 +
 README.md                                     | 125 ++++++------
 build.gradle                                  |  16 +-
 feign-gson/README.md                          |  10 +
 .../src/main/java/feign/gson/GsonModule.java  | 127 ++++++++++++
 .../test/java/feign/gson/GsonModuleTest.java  | 182 ++++++++++++++++++
 settings.gradle                               |   2 +-
 7 files changed, 399 insertions(+), 64 deletions(-)
 create mode 100644 feign-gson/README.md
 create mode 100644 feign-gson/src/main/java/feign/gson/GsonModule.java
 create mode 100644 feign-gson/src/test/java/feign/gson/GsonModuleTest.java

diff --git a/CHANGES.md b/CHANGES.md
index 8c6102d972..30ab975ce9 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,6 +1,7 @@
 ### Version 3.0
 * Added support for asynchronous callbacks via `IncrementalCallback` and `IncrementalDecoder.TextStream`.
 * Wire is now Logger, with configurable Logger.Level.
+* Added `feign-gson` codec, used via `new GsonModule()`
 * changed codec to be similar to [WebSocket JSR 356](http://docs.oracle.com/javaee/7/api/javax/websocket/package-summary.html)
   * Decoder is now `Decoder.TextStream`
   * BodyEncoder is now `Encoder.Text`
diff --git a/README.md b/README.md
index 95195ce31d..29ff2e2aa6 100644
--- a/README.md
+++ b/README.md
@@ -34,40 +34,9 @@ public static void main(String... args) {
   }
 }
 ```
-### Decoders
-The last argument to `Feign.create` specifies how to decode the responses, modeled in Dagger.  Here's how it looks to wire in a default gson decoder:
-
-```java
-@Module(overrides = true, library = true)
-static class GsonModule {
-  @Provides(type = SET) Decoder decoder() {
-    return new Decoder.TextStream() {
-      Gson gson = new Gson();
 
-      @Override public Object decode(Reader reader, Type type) throws IOException {
-        try {
-          return gson.fromJson(reader, type);
-        } catch (JsonIOException e) {
-          if (e.getCause() != null && e.getCause() instanceof IOException) {
-            throw IOException.class.cast(e.getCause());
-          }
-          throw e;
-        }
-      }
-    };
-  }
-}
-```
-Feign doesn't offer a built-in json decoder as you can see above it is very few lines of code to wire yours in.  If you are a jackson user, you'd probably thank us for not dragging in a dependency you don't use.
+Feign includes a fully functional json codec in the `feign-gson` extension.  See the `Decoder` section for how to write your own.
 
-#### Type-specific Decoders
-The generic parameter of `Decoder.TextStream` designates which The type parameter is either a concrete type, or `Object`, if your decoder can handle multiple types.  To add a type-specific decoder, ensure your type parameter is correct.  Here's an example of an xml decoder that will only apply to methods that return `ZoneList`.
-
-```
-@Provides(type = SET) Decoder zoneListDecoder(Provider handlers) {
-  return new SAXDecoder(handlers){};
-}
-```
 ### Asynchronous Incremental Callbacks
 If specified as the last argument of a method `IncrementalCallback` fires a background task to add new elements to the callback as they are decoded.  Think of `IncrementalCallback` as an asynchronous equivalent to a lazy sequence.
 
@@ -91,36 +60,6 @@ IncrementalCallback printlnObserver = new IncrementalCallback`, you'll need to configure an `IncrementalDecoderi.TextStream` or a general one for all types (`IncrementalDecoder.TextStream`).
-
-Here's how to wire in a reflective incremental json decoder:
-```java
-@Provides(type = SET) IncrementalDecoder incrementalDecoder(final Gson gson) {
-  return new IncrementalDecoder.TextStream() {
-
-    @Override
-    public void decode(Reader reader, Type type, IncrementalCallback incrementalCallback) throws IOException {
-      JsonReader jsonReader = new JsonReader(reader);
-      jsonReader.beginArray();
-      while (jsonReader.hasNext()) {
-        try {
-          incrementalCallback.onNext(gson.fromJson(jsonReader, type));
-        } catch (JsonIOException e) {
-          if (e.getCause() != null && e.getCause() instanceof IOException) {
-            throw IOException.class.cast(e.getCause());
-          }
-          throw e;
-        }
-      }
-      jsonReader.endArray();
-    }
-  };
-}
-```
-
-
-
 ### Multiple Interfaces
 Feign can produce multiple api interfaces.  These are defined as `Target` (default `HardCodedTarget`), which allow for dynamic discovery and decoration of requests prior to execution.
 
@@ -134,6 +73,14 @@ You can find [several examples](https://github.com/Netflix/feign/tree/master/fei
 
 ### Integrations
 Feign intends to work well within Netflix and other Open Source communities.  Modules are welcome to integrate with your favorite projects!
+### Gson
+[GsonModule](https://github.com/Netflix/feign/tree/master/feign-gson) adds default encoders and decoders so you get get started with a json api.
+
+Integration requires you pass `new GsonModule()` to `Feign.create()`, or add it to your graph with Dagger:
+```java
+GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule());
+```
+
 ### JAX-RS
 [JAXRSModule](https://github.com/Netflix/feign/tree/master/feign-jaxrs) overrides annotation processing to instead use standard ones supplied by the JAX-RS specification.  This is currently targeted at the 1.1 spec.
 
@@ -151,6 +98,60 @@ Integration requires you to pass your ribbon client name as the host part of the
 ```java
 MyService api = Feign.create(MyService.class, "https://myAppProd", new RibbonModule());
 ```
+
+### Decoders
+The last argument to `Feign.create` allows you to specify additional configuration such as how to decode a responses, modeled in Dagger.
+
+If any methods in your interface return types besides `void` or `String`, you'll need to configure a `Decoder.TextStream` or a general one for all types (`Decoder.TextStream`).
+
+The `GsonModule` in the `feign-gson` extension configures a (`Decoder.TextStream`) which parses objects from json using reflection.
+
+Here's how you could write this yourself, using whatever library you prefer:
+```java
+@Module(overrides = true, library = true)
+static class JsonModule {
+  @Provides(type = SET) Decoder decoder(final JsonParser parser) {
+    return new Decoder.TextStream() {
+
+      @Override public Object decode(Reader reader, Type type) throws IOException {
+        return parser.readJson(reader, type);
+      }
+
+    };
+  }
+}
+```
+#### Type-specific Decoders
+The generic parameter of `Decoder.TextStream` designates which The type parameter is either a concrete type, or `Object`, if your decoder can handle multiple types.  To add a type-specific decoder, ensure your type parameter is correct.  Here's an example of an xml decoder that will only apply to methods that return `ZoneList`.
+
+```
+@Provides(type = SET) Decoder zoneListDecoder(Provider handlers) {
+  return new SAXDecoder(handlers){};
+}
+```
+#### Incremental Decoding
+The last argument to `Feign.create` allows you to specify additional configuration such as how to decode a responses, modeled in Dagger.
+
+When using an `IncrementalCallback`, if `T` is not `Void` or `String`, you'll need to configure an `IncrementalDecoder.TextStream` or a general one for all types (`IncrementalDecoder.TextStream`).
+
+The `GsonModule` in the `feign-gson` extension configures a (`IncrementalDecoder.TextStream`) which parses objects from json using reflection.
+
+Here's how you could write this yourself, using whatever library you prefer:
+```java
+@Provides(type = SET) IncrementalDecoder incrementalDecoder(final JsonParser parser) {
+  return new IncrementalDecoder.TextStream() {
+
+    @Override
+    public void decode(Reader reader, Type type, IncrementalCallback incrementalCallback) throws IOException {
+      jsonReader.beginArray();
+      while (jsonReader.hasNext()) {
+        incrementalCallback.onNext(parser.readJson(reader, type));
+      }
+      jsonReader.endArray();
+    }
+  };
+}
+```
 ### Advanced usage and Dagger
 #### Dagger
 Feign can be directly wired into Dagger which keeps things at compile time and Android friendly.  As opposed to exposing builders for config, Feign intends users to embed their config in Dagger.
diff --git a/build.gradle b/build.gradle
index 56d1cd4312..829eb46df8 100644
--- a/build.gradle
+++ b/build.gradle
@@ -61,7 +61,21 @@ project(':feign-jaxrs') {
         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 'com.google.mockwebserver:mockwebserver:20130505'
+    }
+}
+
+project(':feign-gson') {
+    apply plugin: 'java'
+
+    test {
+        useTestNG()
+    }
+
+    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'
     }
 }
 
diff --git a/feign-gson/README.md b/feign-gson/README.md
new file mode 100644
index 0000000000..206990e74b
--- /dev/null
+++ b/feign-gson/README.md
@@ -0,0 +1,10 @@
+Gson Codec
+===================
+
+This module adds support for encoding and decoding json via the Gson library.
+
+Add this to your object graph like so:
+
+```java
+GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule());
+```
diff --git a/feign-gson/src/main/java/feign/gson/GsonModule.java b/feign-gson/src/main/java/feign/gson/GsonModule.java
new file mode 100644
index 0000000000..53cc8ac0d5
--- /dev/null
+++ b/feign-gson/src/main/java/feign/gson/GsonModule.java
@@ -0,0 +1,127 @@
+/*
+ * 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.gson;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.InstanceCreator;
+import com.google.gson.JsonIOException;
+import com.google.gson.TypeAdapter;
+import com.google.gson.internal.ConstructorConstructor;
+import com.google.gson.internal.bind.MapTypeAdapterFactory;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import dagger.Provides;
+import feign.IncrementalCallback;
+import feign.codec.Decoder;
+import feign.codec.EncodeException;
+import feign.codec.Encoder;
+import feign.codec.IncrementalDecoder;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import java.io.IOException;
+import java.io.Reader;
+import java.lang.reflect.Type;
+import java.util.Collections;
+import java.util.Map;
+
+import static dagger.Provides.Type.SET;
+
+@dagger.Module(library = true, overrides = true)
+public final class GsonModule {
+
+  @Provides(type = SET) Encoder encoder(GsonCodec codec) {
+    return codec;
+  }
+
+  @Provides(type = SET) Decoder decoder(GsonCodec codec) {
+    return codec;
+  }
+
+  @Provides(type = SET) IncrementalDecoder incrementalDecoder(GsonCodec codec) {
+    return codec;
+  }
+
+  static class GsonCodec implements Encoder.Text, Decoder.TextStream, IncrementalDecoder.TextStream {
+    private final Gson gson;
+
+    @Inject GsonCodec(Gson gson) {
+      this.gson = gson;
+    }
+
+    @Override public String encode(Object object) throws EncodeException {
+      return gson.toJson(object);
+    }
+
+    @Override public Object decode(Reader reader, Type type) throws IOException {
+      return fromJson(new JsonReader(reader), type);
+    }
+
+    @Override
+    public void decode(Reader reader, Type type, IncrementalCallback incrementalCallback) throws IOException {
+      JsonReader jsonReader = new JsonReader(reader);
+      jsonReader.beginArray();
+      while (jsonReader.hasNext()) {
+        incrementalCallback.onNext(fromJson(jsonReader, type));
+      }
+      jsonReader.endArray();
+    }
+
+    private Object fromJson(JsonReader jsonReader, Type type) throws IOException {
+      try {
+        return gson.fromJson(jsonReader, type);
+      } catch (JsonIOException e) {
+        if (e.getCause() != null && e.getCause() instanceof IOException) {
+          throw IOException.class.cast(e.getCause());
+        }
+        throw e;
+      }
+    }
+  }
+
+  // deals with scenario where gson Object type treats all numbers as doubles.
+  @Provides TypeAdapter> doubleToInt() {
+    return new TypeAdapter>() {
+      TypeAdapter> delegate = new MapTypeAdapterFactory(new ConstructorConstructor(
+          Collections.>emptyMap()), false).create(new Gson(), token);
+
+      @Override
+      public void write(JsonWriter out, Map value) throws IOException {
+        delegate.write(out, value);
+      }
+
+      @Override
+      public Map read(JsonReader in) throws IOException {
+        Map map = delegate.read(in);
+        for (Map.Entry entry : map.entrySet()) {
+          if (entry.getValue() instanceof Double) {
+            entry.setValue(Double.class.cast(entry.getValue()).intValue());
+          }
+        }
+        return map;
+      }
+    }.nullSafe();
+  }
+
+  @Provides @Singleton Gson gson(TypeAdapter> doubleToInt) {
+    return new GsonBuilder().registerTypeAdapter(token.getType(), doubleToInt).setPrettyPrinting().create();
+  }
+
+  protected final static TypeToken> token = new TypeToken>() {
+  };
+}
diff --git a/feign-gson/src/test/java/feign/gson/GsonModuleTest.java b/feign-gson/src/test/java/feign/gson/GsonModuleTest.java
new file mode 100644
index 0000000000..9dd61a9826
--- /dev/null
+++ b/feign-gson/src/test/java/feign/gson/GsonModuleTest.java
@@ -0,0 +1,182 @@
+/*
+ * 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.gson;
+
+import com.google.gson.reflect.TypeToken;
+import dagger.Module;
+import dagger.ObjectGraph;
+import feign.IncrementalCallback;
+import feign.codec.Decoder;
+import feign.codec.Encoder;
+import feign.codec.IncrementalDecoder;
+import org.testng.annotations.Test;
+
+import javax.inject.Inject;
+import java.io.StringReader;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.fail;
+
+@Test
+public class GsonModuleTest {
+
+  @Test public void providesEncoderDecoderAndIncrementalDecoder() throws Exception {
+    @Module(includes = GsonModule.class, injects = SetBindings.class) class SetBindings {
+      @Inject Set encoders;
+      @Inject Set decoders;
+      @Inject Set incrementalDecoders;
+    }
+
+    SetBindings bindings = new SetBindings();
+    ObjectGraph.create(bindings).inject(bindings);
+
+    assertEquals(bindings.encoders.size(), 1);
+    assertEquals(bindings.encoders.iterator().next().getClass(), GsonModule.GsonCodec.class);
+    assertEquals(bindings.decoders.size(), 1);
+    assertEquals(bindings.decoders.iterator().next().getClass(), GsonModule.GsonCodec.class);
+    assertEquals(bindings.incrementalDecoders.size(), 1);
+    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 encoders;
+    }
+
+    SetBindings bindings = new SetBindings();
+    ObjectGraph.create(bindings).inject(bindings);
+
+    Map map = new LinkedHashMap();
+    map.put("foo", 1);
+
+    assertEquals(Encoder.Text.class.cast(bindings.encoders.iterator().next()).encode(map), ""//
+        + "{\n" //
+        + "  \"foo\": 1\n" //
+        + "}");
+  }
+
+  @Test public void encodesFormParams() throws Exception {
+    @Module(includes = GsonModule.class, injects = SetBindings.class) class SetBindings {
+      @Inject Set encoders;
+    }
+
+    SetBindings bindings = new SetBindings();
+    ObjectGraph.create(bindings).inject(bindings);
+
+    Map form = new LinkedHashMap();
+    form.put("foo", 1);
+    form.put("bar", Arrays.asList(2, 3));
+
+    assertEquals(Encoder.Text.class.cast(bindings.encoders.iterator().next()).encode(form), ""//
+        + "{\n" //
+        + "  \"foo\": 1,\n" //
+        + "  \"bar\": [\n" //
+        + "    2,\n" //
+        + "    3\n" //
+        + "  ]\n" //
+        + "}");
+  }
+
+  static class Zone extends LinkedHashMap {
+    Zone() {
+      // for reflective instantiation.
+    }
+
+    Zone(String name) {
+      this(name, null);
+    }
+
+    Zone(String name, String id) {
+      put("name", name);
+      if (id != null)
+        put("id", id);
+    }
+
+    private static final long serialVersionUID = 1L;
+  }
+
+  @Test public void decodes() throws Exception {
+    @Module(includes = GsonModule.class, injects = SetBindings.class) class SetBindings {
+      @Inject Set decoders;
+    }
+
+    SetBindings bindings = new SetBindings();
+    ObjectGraph.create(bindings).inject(bindings);
+
+    List zones = new LinkedList();
+    zones.add(new Zone("denominator.io."));
+    zones.add(new Zone("denominator.io.", "ABCD"));
+
+    assertEquals(Decoder.TextStream.class.cast(bindings.decoders.iterator().next())
+        .decode(new StringReader(zonesJson), new TypeToken>() {
+        }.getType()), zones);
+  }
+
+  @Test public void decodesIncrementally() throws Exception {
+    @Module(includes = GsonModule.class, injects = SetBindings.class) class SetBindings {
+      @Inject Set decoders;
+    }
+
+    SetBindings bindings = new SetBindings();
+    ObjectGraph.create(bindings).inject(bindings);
+
+    final List zones = new LinkedList();
+    zones.add(new Zone("denominator.io."));
+    zones.add(new Zone("denominator.io.", "ABCD"));
+
+    final AtomicInteger index = new AtomicInteger(0);
+
+    IncrementalCallback zoneCallback = new IncrementalCallback() {
+
+      @Override public void onNext(Zone element) {
+        assertEquals(element, zones.get(index.getAndIncrement()));
+      }
+
+      @Override public void onSuccess() {
+        // decoder shouldn't call onSuccess
+        fail();
+      }
+
+      @Override public void onFailure(Throwable cause) {
+        // decoder shouldn't call onFailure
+        fail();
+      }
+    };
+
+    IncrementalDecoder.TextStream.class.cast(bindings.decoders.iterator().next())
+        .decode(new StringReader(zonesJson), Zone.class, zoneCallback);
+
+    assertEquals(index.get(), 2);
+  }
+
+  private String zonesJson = ""//
+      + "[\n"//
+      + "  {\n"//
+      + "    \"name\": \"denominator.io.\"\n"//
+      + "  },\n"//
+      + "  {\n"//
+      + "    \"name\": \"denominator.io.\",\n"//
+      + "    \"id\": \"ABCD\"\n"//
+      + "  }\n"//
+      + "]\n";
+}
diff --git a/settings.gradle b/settings.gradle
index dc5b04fffb..f15a2c3970 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,2 +1,2 @@
 rootProject.name='feign'
-include 'feign-core', 'feign-jaxrs', 'feign-ribbon', 'examples:feign-example-cli'
+include 'feign-core', 'feign-gson', 'feign-jaxrs', 'feign-ribbon', 'examples:feign-example-cli'

From a590c2dc2d746ad40217df92013ff75556a70ecb Mon Sep 17 00:00:00 2001
From: adriancole 
Date: Mon, 15 Jul 2013 20:14:19 -0700
Subject: [PATCH 072/179] bumped to 4.0.0-SNAPSHOT

---
 gradle.properties | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gradle.properties b/gradle.properties
index cd92d6b085..5594a271c9 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1 +1 @@
-version=3.0.0-SNAPSHOT
+version=4.0.0-SNAPSHOT

From 6731b53283343352a2dd4022b0134d92720914b5 Mon Sep 17 00:00:00 2001
From: adriancole 
Date: Mon, 15 Jul 2013 21:55:27 -0700
Subject: [PATCH 073/179] ported example to use latest and greatest

---
 examples/feign-example-cli/build.gradle       |  4 +-
 .../java/feign/example/cli/GitHubExample.java | 70 +++++++++++--------
 2 files changed, 41 insertions(+), 33 deletions(-)

diff --git a/examples/feign-example-cli/build.gradle b/examples/feign-example-cli/build.gradle
index 1a5882372d..55b0af2dea 100644
--- a/examples/feign-example-cli/build.gradle
+++ b/examples/feign-example-cli/build.gradle
@@ -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'
 }
 
diff --git a/examples/feign-example-cli/src/main/java/feign/example/cli/GitHubExample.java b/examples/feign-example-cli/src/main/java/feign/example/cli/GitHubExample.java
index 48597d7e54..3106e5116a 100644
--- a/examples/feign-example-cli/src/main/java/feign/example/cli/GitHubExample.java
+++ b/examples/feign-example-cli/src/main/java/feign/example/cli/GitHubExample.java
@@ -15,22 +15,14 @@
  */
 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.IncrementalCallback;
 import feign.RequestLine;
-import feign.codec.Decoder;
+import feign.gson.GsonModule;
+
+import javax.inject.Named;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
 
 /**
  * adapted from {@code com.example.retrofit.GitHubClient}
@@ -40,6 +32,10 @@ public class GitHubExample {
   interface GitHub {
     @RequestLine("GET /repos/{owner}/{repo}/contributors")
     List 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 contributors);
   }
 
   static class Contributor {
@@ -47,33 +43,45 @@ static class Contributor {
     int contributions;
   }
 
-  public static void main(String... args) {
+  public static void main(String... args) throws InterruptedException {
     GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule());
 
-    // Fetch and print a list of the contributors to this library.
+    System.out.println("Let's fetch and print a list of the contributors to this library.");
     List 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 decoders() {
-      Map decoders = new LinkedHashMap();
-      decoders.put("GitHub", jsonDecoder);
-      return decoders;
-    }
+    final CountDownLatch latch = new CountDownLatch(1);
+
+    System.out.println("Now, let's do it as an incremental async task.");
+    IncrementalCallback task = new IncrementalCallback() {
+
+      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++;
+      }
 
-    final Decoder jsonDecoder = new Decoder() {
-      Gson gson = new Gson();
+      @Override public void onSuccess() {
+        System.out.println("found " + count + " contributors");
+        latch.countDown();
+      }
 
-      @Override public Object decode(String methodKey, Reader reader, Type type) {
-        return gson.fromJson(reader, type);
+      @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);
   }
 }

From 369c0225354791e57e0e442235f7ad7ba118134c Mon Sep 17 00:00:00 2001
From: adriancole 
Date: Wed, 17 Jul 2013 08:57:41 -0700
Subject: [PATCH 074/179] Replaced IncrementalCallback with full RxJava-style
 Observer support

---
 CHANGES.md                                    |   6 +
 README.md                                     |  24 +-
 build.gradle                                  |  16 +-
 feign-core/src/main/java/feign/Contract.java  | 156 +++++------
 feign-core/src/main/java/feign/Feign.java     |   2 +-
 .../src/main/java/feign/MethodHandler.java    | 252 ++++++++++--------
 .../src/main/java/feign/MethodMetadata.java   |  36 +--
 .../src/main/java/feign/Observable.java       |  39 +++
 ...IncrementalCallback.java => Observer.java} |  25 +-
 .../src/main/java/feign/ReflectiveFeign.java  |  12 +-
 .../src/main/java/feign/Subscription.java     |  32 +++
 .../java/feign/codec/IncrementalDecoder.java  |  33 +--
 .../feign/codec/StringIncrementalDecoder.java |   7 +-
 .../test/java/feign/DefaultContractTest.java  |  47 ++--
 feign-core/src/test/java/feign/FeignTest.java |  61 ++++-
 .../java/feign/examples/GitHubExample.java    |  87 +++---
 .../src/main/java/feign/gson/GsonModule.java  |  10 +-
 .../test/java/feign/gson/GsonModuleTest.java  |   7 +-
 .../main/java/feign/jaxrs/JAXRSModule.java    |   2 +-
 .../java/feign/jaxrs/JAXRSContractTest.java   |  46 ++--
 .../feign/jaxrs/examples/GitHubExample.java   |  88 ++++--
 21 files changed, 605 insertions(+), 383 deletions(-)
 create mode 100644 feign-core/src/main/java/feign/Observable.java
 rename feign-core/src/main/java/feign/{IncrementalCallback.java => Observer.java} (64%)
 create mode 100644 feign-core/src/main/java/feign/Subscription.java

diff --git a/CHANGES.md b/CHANGES.md
index 30ab975ce9..4fb4714af8 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,9 @@
+### Version 4.0
+* Support RxJava-style Observers.
+  * Return type can be `Observable` for an async equiv of `Iterable`.
+  * `Observer` replaces `IncrementalCallback` and is passed to `Observable.subscribe()`.
+  * On `Subscription.unsubscribe()`, `Observer.onNext()` will stop being called.
+
 ### Version 3.0
 * Added support for asynchronous callbacks via `IncrementalCallback` and `IncrementalDecoder.TextStream`.
 * Wire is now Logger, with configurable Logger.Level.
diff --git a/README.md b/README.md
index 29ff2e2aa6..c3349f60ce 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
 # Feign makes writing java http clients easier
-Feign is a java to http client binder inspired by [Dagger](https://github.com/square/dagger), [Retrofit](https://github.com/square/retrofit), [JAXRS-2.0](https://jax-rs-spec.java.net/nonav/2.0/apidocs/index.html), and [WebSockets](http://www.oracle.com/technetwork/articles/java/jsr356-1937161.html).  Feign's first goal was reducing the complexity of binding [Denominator](https://github.com/Netflix/Denominator) uniformly to http apis regardless of [restfulness](http://www.slideshare.net/adrianfcole/99problems).
+Feign is a java to http client binder inspired by [Dagger](https://github.com/square/dagger), [Retrofit](https://github.com/square/retrofit), [RxJava](https://github.com/Netflix/RxJava), [JAXRS-2.0](https://jax-rs-spec.java.net/nonav/2.0/apidocs/index.html), and [WebSocket](http://www.oracle.com/technetwork/articles/java/jsr356-1937161.html).  Feign's first goal was reducing the complexity of binding [Denominator](https://github.com/Netflix/Denominator) uniformly to http apis regardless of [restfulness](http://www.slideshare.net/adrianfcole/99problems).
 
 ### Why Feign and not X?
 
@@ -37,12 +37,20 @@ 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.
 
-### Asynchronous Incremental Callbacks
-If specified as the last argument of a method `IncrementalCallback` fires a background task to add new elements to the callback as they are decoded.  Think of `IncrementalCallback` as an asynchronous equivalent to a lazy sequence.
+### Observable Methods
+If specified as the last return type of a method `Observable` will invoke a new http request for each call to `subscribe()`.  This is the async equivalent to an `Iterable`.
+Here's how one looks:
+```java
+Observable observable = github.contributorsObservable("netflix", "feign");
+subscription = observable.subscribe(newObserver());
+subscription = observable.subscribe(newObserver());
+```
+
+`Observer` is fired as a background which adds new elements as they are decoded, or until `subscription.unsubscribe()` is called.  Think of `Observer` as an asynchronous equivalent to a lazy sequence.
 
 Here's how one looks:
 ```java
-IncrementalCallback printlnObserver = new IncrementalCallback() {
+Observer printlnObserver = new Observer() {
 
   public int count;
 
@@ -58,8 +66,10 @@ IncrementalCallback printlnObserver = new IncrementalCallback` (default `HardCodedTarget`), which allow for dynamic discovery and decoration of requests prior to execution.
 
@@ -142,10 +152,10 @@ Here's how you could write this yourself, using whatever library you prefer:
   return new IncrementalDecoder.TextStream() {
 
     @Override
-    public void decode(Reader reader, Type type, IncrementalCallback incrementalCallback) throws IOException {
+    public void decode(Reader reader, Type type, IncrementalCallback observer) throws IOException {
       jsonReader.beginArray();
       while (jsonReader.hasNext()) {
-        incrementalCallback.onNext(parser.readJson(reader, type));
+        observer.onNext(parser.readJson(reader, type));
       }
       jsonReader.endArray();
     }
diff --git a/build.gradle b/build.gradle
index 829eb46df8..b47bdfa428 100644
--- a/build.gradle
+++ b/build.gradle
@@ -45,7 +45,7 @@ project(':feign-core') {
     }
 }
 
-project(':feign-jaxrs') {
+project(':feign-gson') {
     apply plugin: 'java'
 
     test {
@@ -54,17 +54,13 @@ project(':feign-jaxrs') {
 
     dependencies {
         compile     project(':feign-core')
-        compile     'javax.ws.rs:jsr311-api:1.1.1'
+        compile     'com.google.code.gson:gson:2.2.4'
         provided    'com.squareup.dagger:dagger-compiler:1.0.1'
-        // 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'
     }
 }
 
-project(':feign-gson') {
+project(':feign-jaxrs') {
     apply plugin: 'java'
 
     test {
@@ -73,8 +69,12 @@ project(':feign-gson') {
 
     dependencies {
         compile     project(':feign-core')
-        compile     'com.google.code.gson:gson:2.2.4'
+        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'
     }
 }
diff --git a/feign-core/src/main/java/feign/Contract.java b/feign-core/src/main/java/feign/Contract.java
index f9b6d7d29b..eed9b7bd1a 100644
--- a/feign-core/src/main/java/feign/Contract.java
+++ b/feign-core/src/main/java/feign/Contract.java
@@ -31,99 +31,105 @@
 /**
  * Defines what annotations and values are valid on interfaces.
  */
-public abstract class Contract {
+public interface Contract {
 
   /**
    * Called to parse the methods in the class that are linked to HTTP requests.
    */
-  public List parseAndValidatateMetadata(Class declaring) {
-    List metadata = new ArrayList();
-    for (Method method : declaring.getDeclaredMethods()) {
-      if (method.getDeclaringClass() == Object.class)
-        continue;
-      metadata.add(parseAndValidatateMetadata(method));
-    }
-    return metadata;
-  }
+  List parseAndValidatateMetadata(Class declaring);
 
-  /**
-   * Called indirectly by {@link #parseAndValidatateMetadata(Class)}.
-   */
-  public MethodMetadata parseAndValidatateMetadata(Method method) {
-    MethodMetadata data = new MethodMetadata();
-    data.decodeInto(method.getGenericReturnType());
-    data.configKey(Feign.configKey(method));
+  public static abstract class BaseContract implements Contract {
 
-    for (Annotation methodAnnotation : method.getAnnotations()) {
-      processAnnotationOnMethod(data, methodAnnotation, method);
+    @Override public List parseAndValidatateMetadata(Class declaring) {
+      List metadata = new ArrayList();
+      for (Method method : declaring.getDeclaredMethods()) {
+        if (method.getDeclaringClass() == Object.class)
+          continue;
+        metadata.add(parseAndValidatateMetadata(method));
+      }
+      return metadata;
     }
-    checkState(data.template().method() != null, "Method %s not annotated with HTTP method type (ex. GET, POST)",
-        method.getName());
-    Class[] parameterTypes = method.getParameterTypes();
 
-    Annotation[][] parameterAnnotations = method.getParameterAnnotations();
-    int count = parameterAnnotations.length;
-    for (int i = 0; i < count; i++) {
-      boolean isHttpAnnotation = false;
-      if (parameterAnnotations[i] != null) {
-        isHttpAnnotation = processAnnotationsOnParameter(data, parameterAnnotations[i], i);
+    /**
+     * Called indirectly by {@link #parseAndValidatateMetadata(Class)}.
+     */
+    public MethodMetadata parseAndValidatateMetadata(Method method) {
+      MethodMetadata data = new MethodMetadata();
+      data.returnType(method.getGenericReturnType());
+      data.configKey(Feign.configKey(method));
+
+      if (Observable.class.isAssignableFrom(method.getReturnType())) {
+        Type context = method.getGenericReturnType();
+        Type observableType = resolveLastTypeParameter(method.getGenericReturnType(), Observable.class);
+        checkState(observableType != null, "Expected param %s to be Observable or Observable or a subtype",
+            context, observableType);
+        data.incrementalType(observableType);
+      }
+
+      for (Annotation methodAnnotation : method.getAnnotations()) {
+        processAnnotationOnMethod(data, methodAnnotation, method);
       }
-      if (parameterTypes[i] == URI.class) {
-        data.urlIndex(i);
-      } else if (IncrementalCallback.class.isAssignableFrom(parameterTypes[i])) {
-        checkState(method.getReturnType() == void.class, "IncrementalCallback methods must return void: %s", method);
-        checkState(i == count - 1, "IncrementalCallback must be the last parameter: %s", method);
-        Type context = method.getGenericParameterTypes()[i];
-        Type incrementalCallbackType = resolveLastTypeParameter(context, IncrementalCallback.class);
-        data.decodeInto(incrementalCallbackType);
-        data.incrementalCallbackIndex(i);
-        checkState(incrementalCallbackType != null, "Expected param %s to be IncrementalCallback or IncrementalCallback or a subtype",
-            context, incrementalCallbackType);
-      } else if (!isHttpAnnotation) {
-        checkState(data.formParams().isEmpty(), "Body parameters cannot be used with form parameters.");
-        checkState(data.bodyIndex() == null, "Method has too many Body parameters: %s", method);
-        data.bodyIndex(i);
-        data.bodyType(method.getGenericParameterTypes()[i]);
+      checkState(data.template().method() != null, "Method %s not annotated with HTTP method type (ex. GET, POST)",
+          method.getName());
+      Class[] parameterTypes = method.getParameterTypes();
+
+      Annotation[][] parameterAnnotations = method.getParameterAnnotations();
+      int count = parameterAnnotations.length;
+      for (int i = 0; i < count; i++) {
+        boolean isHttpAnnotation = false;
+        if (parameterAnnotations[i] != null) {
+          isHttpAnnotation = processAnnotationsOnParameter(data, parameterAnnotations[i], i);
+        }
+        if (parameterTypes[i] == URI.class) {
+          data.urlIndex(i);
+        } else if (!isHttpAnnotation) {
+          checkState(!Observer.class.isAssignableFrom(parameterTypes[i]),
+              "Please return Observer as opposed to passing an Observable arg: %s", method);
+          checkState(data.formParams().isEmpty(), "Body parameters cannot be used with form parameters.");
+          checkState(data.bodyIndex() == null, "Method has too many Body parameters: %s", method);
+          data.bodyIndex(i);
+          data.bodyType(method.getGenericParameterTypes()[i]);
+        }
       }
+      return data;
     }
-    return data;
-  }
 
-  /**
-   * @param data       metadata collected so far relating to the current java method.
-   * @param annotation annotations present on the current method annotation.
-   * @param method     method currently being processed.
-   */
-  protected abstract void processAnnotationOnMethod(MethodMetadata data, Annotation annotation, Method method);
+    /**
+     * @param data       metadata collected so far relating to the current java method.
+     * @param annotation annotations present on the current method annotation.
+     * @param method     method currently being processed.
+     */
+    protected abstract void processAnnotationOnMethod(MethodMetadata data, Annotation annotation, Method method);
 
-  /**
-   * @param data        metadata collected so far relating to the current java method.
-   * @param annotations annotations present on the current parameter annotation.
-   * @param paramIndex  if you find a name in {@code annotations}, call {@link #nameParam(MethodMetadata, String,
-   *                    int)} with this as the last parameter.
-   * @return true if you called {@link #nameParam(MethodMetadata, String, int)} after finding an http-relevant
-   *         annotation.
-   */
-  protected abstract boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex);
+    /**
+     * @param data        metadata collected so far relating to the current java method.
+     * @param annotations annotations present on the current parameter annotation.
+     * @param paramIndex  if you find a name in {@code annotations}, call {@link #nameParam(MethodMetadata, String,
+     *                    int)} with this as the last parameter.
+     * @return true if you called {@link #nameParam(MethodMetadata, String, int)} after finding an http-relevant
+     *         annotation.
+     */
+    protected abstract boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex);
 
 
-  protected Collection addTemplatedParam(Collection possiblyNull, String name) {
-    if (possiblyNull == null)
-      possiblyNull = new ArrayList();
-    possiblyNull.add(String.format("{%s}", name));
-    return possiblyNull;
-  }
+    protected Collection addTemplatedParam(Collection possiblyNull, String name) {
+      if (possiblyNull == null)
+        possiblyNull = new ArrayList();
+      possiblyNull.add(String.format("{%s}", name));
+      return possiblyNull;
+    }
 
-  /**
-   * links a parameter name to its index in the method signature.
-   */
-  protected void nameParam(MethodMetadata data, String name, int i) {
-    Collection names = data.indexToName().containsKey(i) ? data.indexToName().get(i) : new ArrayList();
-    names.add(name);
-    data.indexToName().put(i, names);
+    /**
+     * links a parameter name to its index in the method signature.
+     */
+    protected void nameParam(MethodMetadata data, String name, int i) {
+      Collection names = data.indexToName().containsKey(i) ? data.indexToName().get(i) : new ArrayList();
+      names.add(name);
+      data.indexToName().put(i, names);
+    }
   }
 
-  static class Default extends Contract {
+  static class Default extends BaseContract {
 
     @Override
     protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) {
diff --git a/feign-core/src/main/java/feign/Feign.java b/feign-core/src/main/java/feign/Feign.java
index 171116f4dc..d5d103ea55 100644
--- a/feign-core/src/main/java/feign/Feign.java
+++ b/feign-core/src/main/java/feign/Feign.java
@@ -137,7 +137,7 @@ public static class Defaults {
     }
 
     /**
-     * Used for both http invocation and decoding when incrementalCallbacks are used.
+     * Used for both http invocation and decoding when observers are used.
      */
     @Provides @Singleton @Named("http") Executor httpExecutor() {
       return Executors.newCachedThreadPool(new ThreadFactory() {
diff --git a/feign-core/src/main/java/feign/MethodHandler.java b/feign-core/src/main/java/feign/MethodHandler.java
index 42076ff5af..104b338d25 100644
--- a/feign-core/src/main/java/feign/MethodHandler.java
+++ b/feign-core/src/main/java/feign/MethodHandler.java
@@ -29,26 +29,15 @@
 import java.io.Reader;
 import java.util.concurrent.Executor;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import static feign.FeignException.errorExecuting;
 import static feign.FeignException.errorReading;
 import static feign.Util.checkNotNull;
 import static feign.Util.ensureClosed;
 
-abstract class MethodHandler {
-
-  /**
-   * same approach as retrofit: temporarily rename threads
-   */
-  static final String THREAD_PREFIX = "Feign-";
-  static final String IDLE_THREAD_NAME = THREAD_PREFIX + "Idle";
-
-  /**
-   * Those using guava will implement as {@code Function}.
-   */
-  static interface BuildTemplateFromArgs {
-    public RequestTemplate apply(Object[] argv);
-  }
+interface MethodHandler {
+  Object invoke(Object[] argv) throws Throwable;
 
   static class Factory {
 
@@ -74,42 +63,77 @@ public MethodHandler create(Target target, MethodMetadata md, BuildTemplateFr
     }
 
     public MethodHandler create(Target target, MethodMetadata md, BuildTemplateFromArgs buildTemplateFromArgs,
-                                Options options, IncrementalDecoder.TextStream incrementalCallbackDecoder,
+                                Options options, IncrementalDecoder.TextStream incrementalDecoder,
                                 ErrorDecoder errorDecoder) {
-      return new IncrementalCallbackMethodHandler(target, client, retryer, logger, logLevel, md, buildTemplateFromArgs,
-          options, incrementalCallbackDecoder, errorDecoder, httpExecutor);
+      ObserverHandler observerHandler = new ObserverHandler(target, client, retryer, logger, logLevel, md,
+          buildTemplateFromArgs, options, incrementalDecoder, errorDecoder, httpExecutor);
+      return new ObservableMethodHandler(observerHandler);
+    }
+  }
+
+  /**
+   * Those using guava will implement as {@code Function}.
+   */
+  interface BuildTemplateFromArgs {
+    public RequestTemplate apply(Object[] argv);
+  }
+
+  static class ObservableMethodHandler implements MethodHandler {
+    private final ObserverHandler observerHandler;
+
+    private ObservableMethodHandler(ObserverHandler observerHandler) {
+      this.observerHandler = observerHandler;
+    }
+
+    @Override public Object invoke(Object[] argv) {
+      final Object[] argvCopy = new Object[argv != null ? argv.length : 0];
+      if (argv != null)
+        System.arraycopy(argv, 0, argvCopy, 0, argv.length);
+
+      return new Observable() {
+
+        @Override public Subscription subscribe(Observer observer) {
+          final Object[] oneMoreArg = new Object[argvCopy.length + 1];
+          System.arraycopy(argvCopy, 0, oneMoreArg, 0, argvCopy.length);
+          oneMoreArg[argvCopy.length] = observer;
+          return observerHandler.invoke(oneMoreArg);
+        }
+      };
     }
   }
 
-  static final class IncrementalCallbackMethodHandler extends MethodHandler {
+  static class ObserverHandler extends BaseMethodHandler {
     private final Lazy httpExecutor;
-    private final IncrementalDecoder.TextStream incDecoder;
+    private final IncrementalDecoder.TextStream incrementalDecoder;
 
-    private IncrementalCallbackMethodHandler(Target target, Client client, Provider retryer, Logger logger,
-                                             Logger.Level logLevel, MethodMetadata metadata,
-                                             BuildTemplateFromArgs buildTemplateFromArgs, Options options,
-                                             IncrementalDecoder.TextStream incDecoder, ErrorDecoder errorDecoder,
-                                             Lazy httpExecutor) {
+    private ObserverHandler(Target target, Client client, Provider retryer, Logger logger,
+                            Logger.Level logLevel, MethodMetadata metadata, BuildTemplateFromArgs buildTemplateFromArgs,
+                            Options options, IncrementalDecoder.TextStream incrementalDecoder,
+                            ErrorDecoder errorDecoder, Lazy httpExecutor) {
       super(target, client, retryer, logger, logLevel, metadata, buildTemplateFromArgs, options, errorDecoder);
       this.httpExecutor = checkNotNull(httpExecutor, "httpExecutor for %s", target);
-      this.incDecoder = checkNotNull(incDecoder, "incrementalCallbackDecoder for %s", target);
+      this.incrementalDecoder = checkNotNull(incrementalDecoder, "incrementalDecoder for %s", target);
     }
 
-    @Override public Object invoke(final Object[] argv) throws Throwable {
+    @Override public Subscription invoke(Object[] argv) {
+      final AtomicBoolean subscribed = new AtomicBoolean(true);
+      final Object[] oneMoreArg = new Object[argv.length + 1];
+      System.arraycopy(argv, 0, oneMoreArg, 0, argv.length);
+      oneMoreArg[argv.length] = subscribed;
       httpExecutor.get().execute(new Runnable() {
         @Override public void run() {
           Error error = null;
-          Object arg = argv[metadata.incrementalCallbackIndex()];
-          IncrementalCallback incrementalCallback = IncrementalCallback.class.cast(arg);
+          Object arg = oneMoreArg[oneMoreArg.length - 2];
+          Observer observer = Observer.class.cast(arg);
           try {
-            IncrementalCallbackMethodHandler.super.invoke(argv);
-            incrementalCallback.onSuccess();
+            ObserverHandler.super.invoke(oneMoreArg);
+            observer.onSuccess();
           } catch (Error cause) {
             // assign to a variable in case .onFailure throws a RTE
             error = cause;
-            incrementalCallback.onFailure(cause);
+            observer.onFailure(cause);
           } catch (Throwable cause) {
-            incrementalCallback.onFailure(cause);
+            observer.onFailure(cause);
           } finally {
             Thread.currentThread().setName(IDLE_THREAD_NAME);
             if (error != null)
@@ -117,26 +141,31 @@ private IncrementalCallbackMethodHandler(Target target, Client client, Provid
           }
         }
       });
-      return null; // void.
+      return new Subscription() {
+        @Override public void unsubscribe() {
+          subscribed.set(false);
+        }
+      };
     }
 
-    @Override protected Object decode(Object[] argv, Response response) throws Throwable {
-      Object arg = argv[metadata.incrementalCallbackIndex()];
-      IncrementalCallback incrementalCallback = IncrementalCallback.class.cast(arg);
-      if (metadata.decodeInto().equals(Response.class)) {
-        incrementalCallback.onNext(response);
-      } else if (metadata.decodeInto() != Void.class) {
+    @Override protected Void decode(Object[] oneMoreArg, Response response) throws IOException {
+      Object arg = oneMoreArg[oneMoreArg.length - 2];
+      Observer observer = Observer.class.cast(arg);
+      AtomicBoolean subscribed = AtomicBoolean.class.cast(oneMoreArg[oneMoreArg.length - 1]);
+      if (metadata.incrementalType().equals(Response.class)) {
+        observer.onNext(response);
+      } else if (metadata.incrementalType() != Void.class) {
         Response.Body body = response.body();
         if (body == null)
           return null;
         Reader reader = body.asReader();
         try {
-          incDecoder.decode(reader, metadata.decodeInto(), incrementalCallback);
+          incrementalDecoder.decode(reader, metadata.incrementalType(), observer, subscribed);
         } finally {
           ensureClosed(body);
         }
       }
-      return null; // void
+      return null;
     }
 
     @Override protected Request targetRequest(RequestTemplate template) {
@@ -146,7 +175,13 @@ private IncrementalCallbackMethodHandler(Target target, Client client, Provid
     }
   }
 
-  static final class SynchronousMethodHandler extends MethodHandler {
+  /**
+   * same approach as retrofit: temporarily rename threads
+   */
+  static String THREAD_PREFIX = "Feign-";
+  static String IDLE_THREAD_NAME = THREAD_PREFIX + "Idle";
+
+  static class SynchronousMethodHandler extends BaseMethodHandler {
     private final Decoder.TextStream decoder;
 
     private SynchronousMethodHandler(Target target, Client client, Provider retryer, Logger logger,
@@ -158,13 +193,13 @@ private SynchronousMethodHandler(Target target, Client client, Provider target, Client client, Provider target;
-  protected final Client client;
-  protected final Provider retryer;
-  protected final Logger logger;
-  protected final Logger.Level logLevel;
-
-  protected final BuildTemplateFromArgs buildTemplateFromArgs;
-  protected final Options options;
-  protected final ErrorDecoder errorDecoder;
-
-  private MethodHandler(Target target, Client client, Provider retryer, Logger logger,
-                        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.logger = checkNotNull(logger, "logger for %s", target);
-    this.logLevel = checkNotNull(logLevel, "logLevel for %s", target);
-    this.metadata = checkNotNull(metadata, "metadata for %s", target);
-    this.buildTemplateFromArgs = checkNotNull(buildTemplateFromArgs, "metadata for %s", target);
-    this.options = checkNotNull(options, "options for %s", target);
-    this.errorDecoder = checkNotNull(errorDecoder, "errorDecoder for %s", target);
-  }
+  static abstract class BaseMethodHandler implements MethodHandler {
 
-  public Object invoke(Object[] argv) throws Throwable {
-    RequestTemplate template = buildTemplateFromArgs.apply(argv);
-    Retryer retryer = this.retryer.get();
-    while (true) {
-      try {
-        return executeAndDecode(argv, template);
-      } catch (RetryableException e) {
-        retryer.continueOrPropagate(e);
-        continue;
-      }
-    }
-  }
+    protected final MethodMetadata metadata;
+    protected final Target target;
+    protected final Client client;
+    protected final Provider retryer;
+    protected final Logger logger;
+    protected final Logger.Level logLevel;
 
-  public Object executeAndDecode(Object[] argv, RequestTemplate template) throws Throwable {
-    Request request = targetRequest(template);
+    protected final BuildTemplateFromArgs buildTemplateFromArgs;
+    protected final Options options;
+    protected final ErrorDecoder errorDecoder;
 
-    if (logLevel.ordinal() > Logger.Level.NONE.ordinal()) {
-      logger.logRequest(target, logLevel, request);
+    private BaseMethodHandler(Target target, Client client, Provider retryer, Logger logger,
+                              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.logger = checkNotNull(logger, "logger for %s", target);
+      this.logLevel = checkNotNull(logLevel, "logLevel for %s", target);
+      this.metadata = checkNotNull(metadata, "metadata for %s", target);
+      this.buildTemplateFromArgs = checkNotNull(buildTemplateFromArgs, "metadata for %s", target);
+      this.options = checkNotNull(options, "options for %s", target);
+      this.errorDecoder = checkNotNull(errorDecoder, "errorDecoder for %s", target);
     }
 
-    Response response;
-    long start = System.nanoTime();
-    try {
-      response = client.execute(request, options);
-    } catch (IOException e) {
-      throw errorExecuting(request, e);
+    @Override public Object invoke(Object[] argv) throws Throwable {
+      RequestTemplate template = buildTemplateFromArgs.apply(argv);
+      Retryer retryer = this.retryer.get();
+      while (true) {
+        try {
+          return executeAndDecode(argv, template);
+        } catch (RetryableException e) {
+          retryer.continueOrPropagate(e);
+          continue;
+        }
+      }
     }
-    long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
 
-    try {
+    public Object executeAndDecode(Object[] argv, RequestTemplate template) throws Throwable {
+      Request request = targetRequest(template);
+
       if (logLevel.ordinal() > Logger.Level.NONE.ordinal()) {
-        response = logger.logAndRebufferResponse(target, logLevel, response, elapsedTime);
+        logger.logRequest(target, logLevel, request);
+      }
+
+      Response response;
+      long start = System.nanoTime();
+      try {
+        response = client.execute(request, options);
+      } catch (IOException e) {
+        throw errorExecuting(request, e);
       }
-      if (response.status() >= 200 && response.status() < 300) {
-        return decode(argv, response);
-      } else {
-        throw errorDecoder.decode(metadata.configKey(), response);
+      long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
+
+      try {
+        if (logLevel.ordinal() > Logger.Level.NONE.ordinal()) {
+          response = logger.logAndRebufferResponse(target, logLevel, response, elapsedTime);
+        }
+        if (response.status() >= 200 && response.status() < 300) {
+          return decode(argv, response);
+        } else {
+          throw errorDecoder.decode(metadata.configKey(), response);
+        }
+      } catch (IOException e) {
+        throw errorReading(request, response, e);
+      } finally {
+        ensureClosed(response.body());
       }
-    } catch (IOException e) {
-      throw errorReading(request, response, e);
-    } finally {
-      ensureClosed(response.body());
     }
-  }
 
-  protected Request targetRequest(RequestTemplate template) {
-    return target.apply(new RequestTemplate(template));
-  }
+    protected Request targetRequest(RequestTemplate template) {
+      return target.apply(new RequestTemplate(template));
+    }
 
-  protected abstract Object decode(Object[] argv, Response response) throws Throwable;
+    protected abstract Object decode(Object[] argv, Response response) throws Throwable;
+  }
 }
diff --git a/feign-core/src/main/java/feign/MethodMetadata.java b/feign-core/src/main/java/feign/MethodMetadata.java
index 5463af09b9..14ca1f1a33 100644
--- a/feign-core/src/main/java/feign/MethodMetadata.java
+++ b/feign-core/src/main/java/feign/MethodMetadata.java
@@ -29,9 +29,10 @@ public final class MethodMetadata implements Serializable {
   }
 
   private String configKey;
-  private transient Type decodeInto;
+  private transient Type returnType;
+  private transient Type incrementalType;
   private Integer urlIndex;
-  private Integer incrementalCallbackIndex;
+  private Integer observerIndex;
   private Integer bodyIndex;
   private transient Type bodyType;
   private RequestTemplate template = new RequestTemplate();
@@ -51,33 +52,36 @@ MethodMetadata configKey(String configKey) {
   }
 
   /**
-   * Method return type unless there is an {@link IncrementalCallback} arg.  In this case, it is the type parameter of the
-   * incrementalCallback.
+   * Method return type.
    */
-  public Type decodeInto() {
-    return decodeInto;
+  public Type returnType() {
+    return returnType;
   }
 
-  MethodMetadata decodeInto(Type decodeInto) {
-    this.decodeInto = decodeInto;
+  MethodMetadata returnType(Type returnType) {
+    this.returnType = returnType;
     return this;
   }
 
-  public Integer urlIndex() {
-    return urlIndex;
+  /**
+   * Type that {@link feign.codec.IncrementalDecoder} must process.  If null,
+   * {@link feign.codec.Decoder} will be used against the {@link #returnType()};
+   */
+  public Type incrementalType() {
+    return incrementalType;
   }
 
-  MethodMetadata urlIndex(Integer urlIndex) {
-    this.urlIndex = urlIndex;
+  MethodMetadata incrementalType(Type incrementalType) {
+    this.incrementalType = incrementalType;
     return this;
   }
 
-  public Integer incrementalCallbackIndex() {
-    return incrementalCallbackIndex;
+  public Integer urlIndex() {
+    return urlIndex;
   }
 
-  MethodMetadata incrementalCallbackIndex(Integer incrementalCallbackIndex) {
-    this.incrementalCallbackIndex = incrementalCallbackIndex;
+  MethodMetadata urlIndex(Integer urlIndex) {
+    this.urlIndex = urlIndex;
     return this;
   }
 
diff --git a/feign-core/src/main/java/feign/Observable.java b/feign-core/src/main/java/feign/Observable.java
new file mode 100644
index 0000000000..0ea6112e84
--- /dev/null
+++ b/feign-core/src/main/java/feign/Observable.java
@@ -0,0 +1,39 @@
+/*
+ * 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;
+
+/**
+ * An {@code Observer} is asynchronous equivalent to an {@code Iterable}.
+ * 
+ * Each call to {@link #subscribe(Observer)} implies a new + * {@link Request HTTP request}. + * + * @param expected value to decode incrementally from the http response. + */ +public interface Observable { + + /** + * Calling subscribe will initiate a new HTTP request which will be + * {@link feign.codec.IncrementalDecoder incrementally decoded} into the + * {@code observer} until it is finished or + * {@link feign.Subscription#unsubscribe()} is called. + * + * @param observer + * @return a {@link Subscription} with which you can stop the streaming of + * events to the {@code observer}. + */ + public Subscription subscribe(Observer observer); +} diff --git a/feign-core/src/main/java/feign/IncrementalCallback.java b/feign-core/src/main/java/feign/Observer.java similarity index 64% rename from feign-core/src/main/java/feign/IncrementalCallback.java rename to feign-core/src/main/java/feign/Observer.java index 90173be566..d0aa6c78c4 100644 --- a/feign-core/src/main/java/feign/IncrementalCallback.java +++ b/feign-core/src/main/java/feign/Observer.java @@ -1,25 +1,26 @@ package feign; /** - * Communicates results as they are {@link feign.codec.Decoder decoded} from - * an {@link Response.Body http response body}. {@link #onNext(Object) onNext} - * will be called for each incremental value of type {@code T}, or not at all - * when there are no values present in the response. Methods that accept - * {@code IncrementalCallback} are asynchronous, which implies background - * processing. + * An {@code Observer} is asynchronous equivalent to an {@code Iterator}. + *

+ * Observers receive results as they are + * {@link feign.codec.IncrementalDecoder decoded} from an + * {@link Response.Body http response body}. {@link #onNext(Object) onNext} + * will be called for each incremental value of type {@code T} until + * {@link feign.Subscription#unsubscribe()} is called or the response is finished. *
* {@link #onSuccess() onSuccess} or {@link #onFailure(Throwable)} onFailure} * will be called when the response is finished, but not both. *
- * {@code IncrementalCallback} can be used as an asynchronous alternative to a + * {@code Observer} can be used as an asynchronous alternative to a * {@code Collection}, or any other use where iterative response parsing is * worth the additional effort to implement this interface. *
*
- * Here's an example of implementing {@code IncrementalCallback}: + * Here's an example of implementing {@code Observer}: *
*

- * IncrementalCallback counter = new IncrementalCallback() {
+ * Observer counter = new Observer() {
  *
  *   public int count;
  *
@@ -35,12 +36,12 @@
  *     System.err.println("sad face after contributor " + count);
  *   }
  * };
- * github.contributors("netflix", "feign", counter);
+ * subscription = github.contributors("netflix", "feign", counter);
  * 
* - * @param expected value to decode + * @param expected value to decode incrementally from the http response. */ -public interface IncrementalCallback { +public interface Observer { /** * Invoked as soon as new data is available. Could be invoked many times or * not at all. diff --git a/feign-core/src/main/java/feign/ReflectiveFeign.java b/feign-core/src/main/java/feign/ReflectiveFeign.java index d4e227dc74..37eebc7dc7 100644 --- a/feign-core/src/main/java/feign/ReflectiveFeign.java +++ b/feign-core/src/main/java/feign/ReflectiveFeign.java @@ -194,30 +194,30 @@ public Map apply(Target key) { } if (encoder == null) { throw new IllegalStateException(format("%s needs @Provides(type = Set) Encoder encoder()" + - "{ // Encoder.Text<%s> or Encoder.Text}", md.bodyType(), md.decodeInto())); + "{ // Encoder.Text<%s> or Encoder.Text}", md.configKey(), md.bodyType())); } buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder); } else { buildTemplate = new BuildTemplateByResolvingArgs(md); } - if (md.incrementalCallbackIndex() != null) { - IncrementalDecoder.TextStream incrementalDecoder = incrementalDecoders.get(md.decodeInto()); + if (md.incrementalType() != null) { + IncrementalDecoder.TextStream incrementalDecoder = incrementalDecoders.get(md.incrementalType()); if (incrementalDecoder == null) { incrementalDecoder = incrementalDecoders.get(Object.class); } if (incrementalDecoder == null) { throw new IllegalStateException(format("%s needs @Provides(type = Set) IncrementalDecoder incrementalDecoder()" + - "{ // IncrementalDecoder.TextStream<%s> or IncrementalDecoder.TextStream}", md.configKey(), md.decodeInto())); + "{ // IncrementalDecoder.TextStream<%s> or IncrementalDecoder.TextStream}", md.configKey(), md.incrementalType())); } result.put(md.configKey(), factory.create(key, md, buildTemplate, options, incrementalDecoder, errorDecoder)); } else { - Decoder.TextStream decoder = decoders.get(md.decodeInto()); + Decoder.TextStream decoder = decoders.get(md.returnType()); if (decoder == null) { decoder = decoders.get(Object.class); } if (decoder == null) { throw new IllegalStateException(format("%s needs @Provides(type = Set) Decoder decoder()" + - "{ // Decoder.TextStream<%s> or Decoder.TextStream}", md.configKey(), md.decodeInto())); + "{ // Decoder.TextStream<%s> or Decoder.TextStream}", md.configKey(), md.returnType())); } result.put(md.configKey(), factory.create(key, md, buildTemplate, options, decoder, errorDecoder)); } diff --git a/feign-core/src/main/java/feign/Subscription.java b/feign-core/src/main/java/feign/Subscription.java new file mode 100644 index 0000000000..1b327f747b --- /dev/null +++ b/feign-core/src/main/java/feign/Subscription.java @@ -0,0 +1,32 @@ +/* + * 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; + +/** + * Subscription returns from {@link Observable#subscribe(Observer)} to allow + * unsubscribing. + */ +public interface Subscription { + + /** + * Stop receiving notifications on the {@link Observer} that was registered + * when this Subscription was received. + *
+ * This allows unregistering an {@link Observer} before it has finished + * receiving all events (ie. before onCompleted is called). + */ + void unsubscribe(); +} diff --git a/feign-core/src/main/java/feign/codec/IncrementalDecoder.java b/feign-core/src/main/java/feign/codec/IncrementalDecoder.java index 30f27a04bf..00e11b4b2d 100644 --- a/feign-core/src/main/java/feign/codec/IncrementalDecoder.java +++ b/feign-core/src/main/java/feign/codec/IncrementalDecoder.java @@ -16,15 +16,16 @@ package feign.codec; import feign.FeignException; -import feign.IncrementalCallback; +import feign.Observer; import java.io.IOException; import java.io.Reader; import java.lang.reflect.Type; +import java.util.concurrent.atomic.AtomicBoolean; /** - * Decodes an HTTP response incrementally into an {@link IncrementalCallback} - * via a series of {@link IncrementalCallback#onNext(Object) onNext} calls. + * Decodes an HTTP response incrementally into an {@link feign.Observer} + * via a series of {@link feign.Observer#onNext(Object) onNext} calls. *

* Invoked when {@link feign.Response#status()} is in the 2xx range. * @@ -36,27 +37,29 @@ public interface IncrementalDecoder { * Implement this to decode a resource to an object into a single object. * If you need to wrap exceptions, please do so via {@link feign.codec.DecodeException}. *
- * Do not call {@link feign.IncrementalCallback#onSuccess() onSuccess} or - * {@link feign.IncrementalCallback#onFailure onFailure}. + * Do not call {@link feign.Observer#onSuccess() onSuccess} or + * {@link feign.Observer#onFailure onFailure}. * - * @param input if {@code Closeable}, no need to close this, as the caller - * manages resources. - * @param type type parameter of {@link feign.IncrementalCallback#onNext}. - * @param incrementalCallback call {@link feign.IncrementalCallback#onNext onNext} - * each time an object of {@code type} is decoded - * from the response. + * @param input if {@code Closeable}, no need to close this, as the caller + * manages resources. + * @param type type parameter of {@link feign.Observer#onNext}. + * @param observer call {@link feign.Observer#onNext onNext} + * each time an object of {@code type} is decoded + * from the response. + * @param subscribed false indicates the observer should no longer receive + * {@link Observer#onNext(Object)} calls. * @throws java.io.IOException will be propagated safely to the caller. * @throws feign.codec.DecodeException when decoding failed due to a checked exception * besides IOException. * @throws feign.FeignException when decoding succeeds, but conveys the operation * failed. */ - void decode(I input, Type type, IncrementalCallback incrementalCallback) + void decode(I input, Type type, Observer observer, AtomicBoolean subscribed) throws IOException, DecodeException, FeignException; /** * Used for text-based apis, follows - * {@link feign.codec.IncrementalDecoder#decode(Object, java.lang.reflect.Type, IncrementalCallback)} + * {@link feign.codec.IncrementalDecoder#decode(Object, java.lang.reflect.Type, feign.Observer, AtomicBoolean)} * semantics, applied to inputs of type {@link java.io.Reader}.
* Ex.
*

@@ -90,12 +93,12 @@ void decode(I input, Type type, IncrementalCallback incrementalCallba * this.gson = gson; * } * - * @Override public void decode(Reader reader, Type type, IncrementalCallback incrementalCallback) throws Exception { + * @Override public void decode(Reader reader, Type type, Observer observer) throws Exception { * JsonReader jsonReader = new JsonReader(reader); * jsonReader.beginArray(); * while (jsonReader.hasNext()) { * try { - * incrementalCallback.onNext(gson.fromJson(jsonReader, type)); + * observer.onNext(gson.fromJson(jsonReader, type)); * } catch (JsonIOException e) { * if (e.getCause() != null && * e.getCause() instanceof IOException) { diff --git a/feign-core/src/main/java/feign/codec/StringIncrementalDecoder.java b/feign-core/src/main/java/feign/codec/StringIncrementalDecoder.java index 3e9dc8e005..a3fa77bae8 100644 --- a/feign-core/src/main/java/feign/codec/StringIncrementalDecoder.java +++ b/feign-core/src/main/java/feign/codec/StringIncrementalDecoder.java @@ -15,18 +15,19 @@ */ package feign.codec; -import feign.IncrementalCallback; +import feign.Observer; import java.io.IOException; import java.io.Reader; import java.lang.reflect.Type; +import java.util.concurrent.atomic.AtomicBoolean; public class StringIncrementalDecoder implements IncrementalDecoder.TextStream { private static final StringDecoder STRING_DECODER = new StringDecoder(); @Override - public void decode(Reader reader, Type type, IncrementalCallback incrementalCallback) + public void decode(Reader reader, Type type, Observer observer, AtomicBoolean subscribed) throws IOException { - incrementalCallback.onNext(STRING_DECODER.decode(reader, type)); + observer.onNext(STRING_DECODER.decode(reader, type)); } } diff --git a/feign-core/src/test/java/feign/DefaultContractTest.java b/feign-core/src/test/java/feign/DefaultContractTest.java index 958a7785fc..aaaaf7ebc4 100644 --- a/feign-core/src/test/java/feign/DefaultContractTest.java +++ b/feign-core/src/test/java/feign/DefaultContractTest.java @@ -31,7 +31,7 @@ import static org.testng.Assert.assertTrue; /** - * Tests interfaces defined per {@link feign.Contract.Default} are interpreted into expected {@link feign + * Tests interfaces defined per {@link Contract.Default} are interpreted into expected {@link feign * .RequestTemplate template} * instances. */ @@ -239,44 +239,39 @@ interface HeaderParams { assertEquals(md.indexToName().get(0), ImmutableSet.of("Auth-Token")); } - interface WithIncrementalCallback { - @RequestLine("GET /") void valid(IncrementalCallback> one); + interface WithObservable { + @RequestLine("GET /") Observable> valid(); - @RequestLine("GET /{path}") void badOrder(IncrementalCallback> one, @Named("path") String path); + @RequestLine("GET /") Observable> wildcardExtends(); - @RequestLine("GET /") Response returnType(IncrementalCallback> one); + @RequestLine("GET /") ParameterizedObservable> subtype(); - @RequestLine("GET /") void wildcardExtends(IncrementalCallback> one); + @RequestLine("GET /") Response returnType(Observable> one); - @RequestLine("GET /") void subtype(ParameterizedIncrementalCallback> one); + @RequestLine("GET /") Observable> alsoObserver(Observer> observer); } - static final List listString = null; - - interface ParameterizedIncrementalCallback> extends IncrementalCallback { + interface ParameterizedObservable> extends Observable { } - @Test public void methodCanHaveIncrementalCallbackParam() throws Exception { - contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("valid", IncrementalCallback.class)); + static final List listString = null; + + @Test public void methodCanHaveObservableReturn() throws Exception { + contract.parseAndValidatateMetadata(WithObservable.class.getDeclaredMethod("valid")); } @Test public void methodMetadataReturnTypeOnObservableMethodIsItsTypeParameter() throws Exception { Type listStringType = getClass().getDeclaredField("listString").getGenericType(); - MethodMetadata md = contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("valid", IncrementalCallback.class)); - assertEquals(md.decodeInto(), listStringType); - md = contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("wildcardExtends", IncrementalCallback.class)); - assertEquals(md.decodeInto(), listStringType); - md = contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("subtype", ParameterizedIncrementalCallback.class)); - assertEquals(md.decodeInto(), listStringType); - } - - @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = ".*the last parameter.*") - public void incrementalCallbackParamMustBeLast() throws Exception { - contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("badOrder", IncrementalCallback.class, String.class)); + MethodMetadata md = contract.parseAndValidatateMetadata(WithObservable.class.getDeclaredMethod("valid")); + assertEquals(md.incrementalType(), listStringType); + md = contract.parseAndValidatateMetadata(WithObservable.class.getDeclaredMethod("wildcardExtends")); + assertEquals(md.incrementalType(), listStringType); + md = contract.parseAndValidatateMetadata(WithObservable.class.getDeclaredMethod("subtype")); + assertEquals(md.incrementalType(), listStringType); } - @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = ".*must return void.*") - public void incrementalCallbackMethodMustReturnVoid() throws Exception { - contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("returnType", IncrementalCallback.class)); + @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "Please return Observer as opposed to passing an Observable arg.*") + public void noObserverArgs() throws Exception { + contract.parseAndValidatateMetadata(WithObservable.class.getDeclaredMethod("alsoObserver", Observer.class)); } } diff --git a/feign-core/src/test/java/feign/FeignTest.java b/feign-core/src/test/java/feign/FeignTest.java index ac91708ba7..a114c5473f 100644 --- a/feign-core/src/test/java/feign/FeignTest.java +++ b/feign-core/src/test/java/feign/FeignTest.java @@ -38,6 +38,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -82,11 +83,11 @@ void login( @RequestLine("GET /{1}/{2}") Response uriParam(@Named("1") String one, URI endpoint, @Named("2") String two); - @RequestLine("POST /") void incrementVoid(IncrementalCallback incrementalCallback); + @RequestLine("POST /") Observable observableVoid(); - @RequestLine("POST /") void incrementString(IncrementalCallback incrementalCallback); + @RequestLine("POST /") Observable observableString(); - @RequestLine("POST /") void incrementResponse(IncrementalCallback incrementalCallback); + @RequestLine("POST /") Observable observableResponse(); @dagger.Module(overrides = true, library = true) static class Module { @@ -118,7 +119,7 @@ static class Module { } @Test - public void incrementVoid() throws IOException, InterruptedException { + public void observableVoid() throws IOException, InterruptedException { final MockWebServer server = new MockWebServer(); server.enqueue(new MockResponse().setResponseCode(200).setBody("foo")); server.play(); @@ -128,7 +129,7 @@ public void incrementVoid() throws IOException, InterruptedException { final AtomicBoolean success = new AtomicBoolean(); - IncrementalCallback incrementalCallback = new IncrementalCallback() { + Observer observer = new Observer() { @Override public void onNext(Void element) { fail("on next isn't valid for void"); @@ -142,7 +143,7 @@ public void incrementVoid() throws IOException, InterruptedException { fail(cause.getMessage()); } }; - api.incrementVoid(incrementalCallback); + api.observableVoid().subscribe(observer); assertTrue(success.get()); assertEquals(server.getRequestCount(), 1); @@ -152,7 +153,7 @@ public void incrementVoid() throws IOException, InterruptedException { } @Test - public void incrementResponse() throws IOException, InterruptedException { + public void observableResponse() throws IOException, InterruptedException { final MockWebServer server = new MockWebServer(); server.enqueue(new MockResponse().setResponseCode(200).setBody("foo")); server.play(); @@ -162,7 +163,7 @@ public void incrementResponse() throws IOException, InterruptedException { final AtomicBoolean success = new AtomicBoolean(); - IncrementalCallback incrementalCallback = new IncrementalCallback() { + Observer observer = new Observer() { @Override public void onNext(Response element) { assertEquals(element.status(), 200); @@ -176,7 +177,7 @@ public void incrementResponse() throws IOException, InterruptedException { fail(cause.getMessage()); } }; - api.incrementResponse(incrementalCallback); + api.observableResponse().subscribe(observer); assertTrue(success.get()); assertEquals(server.getRequestCount(), 1); @@ -196,7 +197,7 @@ public void incrementString() throws IOException, InterruptedException { final AtomicBoolean success = new AtomicBoolean(); - IncrementalCallback incrementalCallback = new IncrementalCallback() { + Observer observer = new Observer() { @Override public void onNext(String element) { assertEquals(element, "foo"); @@ -210,7 +211,7 @@ public void incrementString() throws IOException, InterruptedException { fail(cause.getMessage()); } }; - api.incrementString(incrementalCallback); + api.observableString().subscribe(observer); assertTrue(success.get()); assertEquals(server.getRequestCount(), 1); @@ -219,6 +220,44 @@ public void incrementString() throws IOException, InterruptedException { } } + @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.play(); + + try { + TestInterface api = Feign.create(TestInterface.class, server.getUrl("").toString(), new TestInterface.Module()); + + final CountDownLatch latch = new CountDownLatch(2); + + Observer observer = new Observer() { + + @Override public void onNext(String element) { + assertEquals(element, "foo"); + } + + @Override public void onSuccess() { + latch.countDown(); + } + + @Override public void onFailure(Throwable cause) { + fail(cause.getMessage()); + } + }; + + Observable observable = api.observableString(); + observable.subscribe(observer); + observable.subscribe(observer); + latch.await(); + + assertEquals(server.getRequestCount(), 2); + } finally { + server.shutdown(); + } + } + @Test public void postTemplateParamsResolve() throws IOException, InterruptedException { final MockWebServer server = new MockWebServer(); diff --git a/feign-core/src/test/java/feign/examples/GitHubExample.java b/feign-core/src/test/java/feign/examples/GitHubExample.java index 5ecc8cb1d4..080fd1893a 100644 --- a/feign-core/src/test/java/feign/examples/GitHubExample.java +++ b/feign-core/src/test/java/feign/examples/GitHubExample.java @@ -21,7 +21,9 @@ 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.codec.Decoder; import feign.codec.IncrementalDecoder; @@ -34,6 +36,7 @@ import java.lang.reflect.Type; import java.util.List; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; import static dagger.Provides.Type.SET; @@ -47,8 +50,7 @@ interface GitHub { List 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 contributors); + Observable observable(@Named("owner") String owner, @Named("repo") String repo); } static class Contributor { @@ -57,7 +59,7 @@ static class Contributor { } 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 contributors = github.contributors("netflix", "feign"); @@ -65,32 +67,14 @@ public static void main(String... args) throws InterruptedException { 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 observable = github.observable("netflix", "feign"); - System.out.println("Now, let's do it as an incremental async task."); - IncrementalCallback task = new IncrementalCallback() { + CountDownLatch latch = new CountDownLatch(2); - 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); + System.out.println("Let's add 2 subscribers."); + observable.subscribe(new ContributorObserver(latch)); + observable.subscribe(new ContributorObserver(latch)); // wait for the task to complete. latch.await(); @@ -98,11 +82,24 @@ public static void main(String... args) throws InterruptedException { System.exit(0); } + @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(); + } + } + /** - * Here's how to wire gson deserialization. + * Here's how it looks to wire json codecs. Note, that you can always instead use {@code feign-gson}! */ @Module(overrides = true, library = true) static class GsonModule { + @Provides @Singleton Gson gson() { return new Gson(); } @@ -128,13 +125,12 @@ static class GsonDecoder implements Decoder.TextStream, IncrementalDecod } @Override - public void decode(Reader reader, Type type, IncrementalCallback incrementalCallback) throws IOException { + public void decode(Reader reader, Type type, Observer observer, AtomicBoolean subscribed) throws IOException { JsonReader jsonReader = new JsonReader(reader); jsonReader.beginArray(); - while (jsonReader.hasNext()) { - incrementalCallback.onNext(fromJson(jsonReader, type)); + while (jsonReader.hasNext() && subscribed.get()) { + observer.onNext(fromJson(jsonReader, type)); } - jsonReader.endArray(); } private Object fromJson(JsonReader jsonReader, Type type) throws IOException { @@ -148,4 +144,29 @@ private Object fromJson(JsonReader jsonReader, Type type) throws IOException { } } } + + static class ContributorObserver implements Observer { + + private final CountDownLatch latch; + public int count; + + public ContributorObserver(CountDownLatch latch) { + this.latch = latch; + } + + // 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(); + } + } } diff --git a/feign-gson/src/main/java/feign/gson/GsonModule.java b/feign-gson/src/main/java/feign/gson/GsonModule.java index 53cc8ac0d5..63873e53a7 100644 --- a/feign-gson/src/main/java/feign/gson/GsonModule.java +++ b/feign-gson/src/main/java/feign/gson/GsonModule.java @@ -26,7 +26,7 @@ import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import dagger.Provides; -import feign.IncrementalCallback; +import feign.Observer; import feign.codec.Decoder; import feign.codec.EncodeException; import feign.codec.Encoder; @@ -39,6 +39,7 @@ import java.lang.reflect.Type; import java.util.Collections; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; import static dagger.Provides.Type.SET; @@ -73,13 +74,12 @@ static class GsonCodec implements Encoder.Text, Decoder.TextStream incrementalCallback) throws IOException { + public void decode(Reader reader, Type type, Observer observer, AtomicBoolean subscribed) throws IOException { JsonReader jsonReader = new JsonReader(reader); jsonReader.beginArray(); - while (jsonReader.hasNext()) { - incrementalCallback.onNext(fromJson(jsonReader, type)); + while (subscribed.get() && jsonReader.hasNext()) { + observer.onNext(fromJson(jsonReader, type)); } - jsonReader.endArray(); } private Object fromJson(JsonReader jsonReader, Type type) throws IOException { diff --git a/feign-gson/src/test/java/feign/gson/GsonModuleTest.java b/feign-gson/src/test/java/feign/gson/GsonModuleTest.java index 9dd61a9826..6f7ddb5519 100644 --- a/feign-gson/src/test/java/feign/gson/GsonModuleTest.java +++ b/feign-gson/src/test/java/feign/gson/GsonModuleTest.java @@ -18,7 +18,7 @@ import com.google.gson.reflect.TypeToken; import dagger.Module; import dagger.ObjectGraph; -import feign.IncrementalCallback; +import feign.Observer; import feign.codec.Decoder; import feign.codec.Encoder; import feign.codec.IncrementalDecoder; @@ -32,6 +32,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import static org.testng.Assert.assertEquals; @@ -146,7 +147,7 @@ static class Zone extends LinkedHashMap { final AtomicInteger index = new AtomicInteger(0); - IncrementalCallback zoneCallback = new IncrementalCallback() { + Observer zoneCallback = new Observer() { @Override public void onNext(Zone element) { assertEquals(element, zones.get(index.getAndIncrement())); @@ -164,7 +165,7 @@ static class Zone extends LinkedHashMap { }; IncrementalDecoder.TextStream.class.cast(bindings.decoders.iterator().next()) - .decode(new StringReader(zonesJson), Zone.class, zoneCallback); + .decode(new StringReader(zonesJson), Zone.class, zoneCallback, new AtomicBoolean(true)); assertEquals(index.get(), 2); } diff --git a/feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java b/feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java index e9e2a5dba2..d224516969 100644 --- a/feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java +++ b/feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java @@ -43,7 +43,7 @@ public final class JAXRSModule { return new JAXRSContract(); } - public static final class JAXRSContract extends Contract { + public static final class JAXRSContract extends Contract.BaseContract { @Override protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) { diff --git a/feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java b/feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java index 36888cad83..0612e16066 100644 --- a/feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java +++ b/feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java @@ -19,8 +19,9 @@ import com.google.common.collect.ImmutableSet; import com.google.gson.reflect.TypeToken; import feign.Body; -import feign.IncrementalCallback; import feign.MethodMetadata; +import feign.Observable; +import feign.Observer; import feign.Response; import org.testng.annotations.Test; @@ -263,44 +264,37 @@ interface HeaderParams { assertEquals(md.indexToName().get(0), ImmutableSet.of("Auth-Token")); } - interface WithIncrementalCallback { - @GET @Path("/") void valid(IncrementalCallback> one); + interface WithObservable { + @GET @Path("/") Observable> valid(); - @GET @Path("/{path}") void badOrder(IncrementalCallback> one, @PathParam("path") String path); + @GET @Path("/") Observable> wildcardExtends(); - @GET @Path("/") Response returnType(IncrementalCallback> one); + @GET @Path("/") ParameterizedObservable> subtype(); - @GET @Path("/") void wildcardExtends(IncrementalCallback> one); + @GET @Path("/") Observable> alsoObserver(Observer> observer); + } - @GET @Path("/") void subtype(ParameterizedIncrementalCallback> one); + interface ParameterizedObservable> extends Observable { } static final List listString = null; - interface ParameterizedIncrementalCallback> extends IncrementalCallback { - } - - @Test public void methodCanHaveIncrementalCallbackParam() throws Exception { - contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("valid", IncrementalCallback.class)); + @Test public void methodCanHaveObservableReturn() throws Exception { + contract.parseAndValidatateMetadata(WithObservable.class.getDeclaredMethod("valid")); } @Test public void methodMetadataReturnTypeOnObservableMethodIsItsTypeParameter() throws Exception { Type listStringType = getClass().getDeclaredField("listString").getGenericType(); - MethodMetadata md = contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("valid", IncrementalCallback.class)); - assertEquals(md.decodeInto(), listStringType); - md = contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("wildcardExtends", IncrementalCallback.class)); - assertEquals(md.decodeInto(), listStringType); - md = contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("subtype", ParameterizedIncrementalCallback.class)); - assertEquals(md.decodeInto(), listStringType); - } - - @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = ".*the last parameter.*") - public void incrementalCallbackParamMustBeLast() throws Exception { - contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("badOrder", IncrementalCallback.class, String.class)); + MethodMetadata md = contract.parseAndValidatateMetadata(WithObservable.class.getDeclaredMethod("valid")); + assertEquals(md.incrementalType(), listStringType); + md = contract.parseAndValidatateMetadata(WithObservable.class.getDeclaredMethod("wildcardExtends")); + assertEquals(md.incrementalType(), listStringType); + md = contract.parseAndValidatateMetadata(WithObservable.class.getDeclaredMethod("subtype")); + assertEquals(md.incrementalType(), listStringType); } - @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = ".*must return void.*") - public void incrementalCallbackMethodMustReturnVoid() throws Exception { - contract.parseAndValidatateMetadata(WithIncrementalCallback.class.getDeclaredMethod("returnType", IncrementalCallback.class)); + @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "Please return Observer as opposed to passing an Observable arg.*") + public void noObserverArgs() throws Exception { + contract.parseAndValidatateMetadata(WithObservable.class.getDeclaredMethod("alsoObserver", Observer.class)); } } diff --git a/feign-jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java b/feign-jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java index 722352ea59..80289f112a 100644 --- a/feign-jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java +++ b/feign-jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java @@ -15,23 +15,21 @@ */ package feign.jaxrs.examples; -import com.google.gson.Gson; -import com.google.gson.JsonIOException; import dagger.Module; import dagger.Provides; import feign.Feign; -import feign.codec.Decoder; +import feign.Logger; +import feign.Observable; +import feign.Observer; +import feign.gson.GsonModule; import feign.jaxrs.JAXRSModule; +import javax.inject.Named; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; -import java.io.IOException; -import java.io.Reader; -import java.lang.reflect.Type; import java.util.List; - -import static dagger.Provides.Type.SET; +import java.util.concurrent.CountDownLatch; /** * adapted from {@code com.example.retrofit.GitHubClient} @@ -39,8 +37,11 @@ public class GitHubExample { interface GitHub { - @GET @Path("/repos/{owner}/{repo}/contributors") List contributors( - @PathParam("owner") String owner, @PathParam("repo") String repo); + @GET @Path("/repos/{owner}/{repo}/contributors") + List contributors(@PathParam("owner") String owner, @PathParam("repo") String repo); + + @GET @Path("/repos/{owner}/{repo}/contributors") + Observable observable(@PathParam("owner") String owner, @PathParam("repo") String repo); } static class Contributor { @@ -48,36 +49,67 @@ static class Contributor { int contributions; } - public static void main(String... args) { + public static void main(String... args) throws InterruptedException { GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GitHubModule()); - // Fetch and print a list of the contributors to this library. + System.out.println("Let's fetch and print a list of the contributors to this library."); List contributors = github.contributors("netflix", "feign"); for (Contributor contributor : contributors) { System.out.println(contributor.login + " (" + contributor.contributions + ")"); } + + System.out.println("Let's treat our contributors as an observable."); + Observable observable = github.observable("netflix", "feign"); + + CountDownLatch latch = new CountDownLatch(2); + + System.out.println("Let's add 2 subscribers."); + observable.subscribe(new ContributorObserver(latch)); + observable.subscribe(new ContributorObserver(latch)); + + // wait for the task to complete. + latch.await(); + + System.exit(0); } /** * JAXRSModule tells us to process @GET etc annotations */ - @Module(overrides = true, library = true, includes = JAXRSModule.class) + @Module(overrides = true, library = true, includes = {JAXRSModule.class, GsonModule.class}) static class GitHubModule { - @Provides(type = SET) Decoder decoder() { - return new Decoder.TextStream() { - Gson gson = new Gson(); - - @Override public Object decode(Reader reader, Type type) throws IOException { - try { - return gson.fromJson(reader, type); - } catch (JsonIOException e) { - if (e.getCause() != null && e.getCause() instanceof IOException) { - throw IOException.class.cast(e.getCause()); - } - throw e; - } - } - }; + + @Provides Logger.Level loggingLevel() { + return Logger.Level.BASIC; + } + + @Provides Logger logger() { + return new Logger.ErrorLogger(); + } + } + + static class ContributorObserver implements Observer { + + private final CountDownLatch latch; + public int count; + + public ContributorObserver(CountDownLatch latch) { + this.latch = latch; + } + + // 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(); } } } From 75b15a1fe9415e8e23e27dfe9fa0be0c9d9dcb6f Mon Sep 17 00:00:00 2001 From: adriancole Date: Thu, 18 Jul 2013 14:58:56 -0600 Subject: [PATCH 075/179] log ioexceptions and retries --- CHANGES.md | 3 + feign-core/src/main/java/feign/Logger.java | 75 +++-- .../src/main/java/feign/MethodHandler.java | 41 ++- .../src/test/java/feign/LoggerTest.java | 272 ++++++++++++++++-- 4 files changed, 322 insertions(+), 69 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 4fb4714af8..7f537e9c0a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,9 @@ * `Observer` replaces `IncrementalCallback` and is passed to `Observable.subscribe()`. * On `Subscription.unsubscribe()`, `Observer.onNext()` will stop being called. +### Version 3.1 +* Log when an http request is retried or a response fails due to an IOException. + ### Version 3.0 * Added support for asynchronous callbacks via `IncrementalCallback` and `IncrementalDecoder.TextStream`. * Wire is now Logger, with configurable Logger.Level. diff --git a/feign-core/src/main/java/feign/Logger.java b/feign-core/src/main/java/feign/Logger.java index c9bec2aa55..48853b1f29 100644 --- a/feign-core/src/main/java/feign/Logger.java +++ b/feign-core/src/main/java/feign/Logger.java @@ -17,7 +17,9 @@ 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 enum Level { 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 static class ErrorLogger extends 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 String format(LogRecord record) { } 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 @@ protected void log(Target target, String format, Object... args) { * 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 @@ void logRequest(Target target, Level logLevel, Request request) { 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 @@ Response logAndRebufferResponse(Target target, Level logLevel, Response respo 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 @@ Response logAndRebufferResponse(Target target, Level logLevel, Response respo } 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(); + } } diff --git a/feign-core/src/main/java/feign/MethodHandler.java b/feign-core/src/main/java/feign/MethodHandler.java index 104b338d25..d7cbffff12 100644 --- a/feign-core/src/main/java/feign/MethodHandler.java +++ b/feign-core/src/main/java/feign/MethodHandler.java @@ -45,10 +45,10 @@ static class Factory { private final Lazy httpExecutor; private final Provider retryer; private final Logger logger; - private final Logger.Level logLevel; + private final Provider logLevel; @Inject Factory(Client client, @Named("http") Lazy httpExecutor, Provider retryer, Logger logger, - Logger.Level logLevel) { + Provider logLevel) { this.client = checkNotNull(client, "client"); this.httpExecutor = checkNotNull(httpExecutor, "httpExecutor"); this.retryer = checkNotNull(retryer, "retryer"); @@ -107,9 +107,10 @@ static class ObserverHandler extends BaseMethodHandler { private final IncrementalDecoder.TextStream incrementalDecoder; private ObserverHandler(Target target, Client client, Provider retryer, Logger logger, - Logger.Level logLevel, MethodMetadata metadata, BuildTemplateFromArgs buildTemplateFromArgs, - Options options, IncrementalDecoder.TextStream incrementalDecoder, - ErrorDecoder errorDecoder, Lazy httpExecutor) { + Provider logLevel, MethodMetadata metadata, + BuildTemplateFromArgs buildTemplateFromArgs, Options options, + IncrementalDecoder.TextStream incrementalDecoder, ErrorDecoder errorDecoder, + Lazy httpExecutor) { super(target, client, retryer, logger, logLevel, metadata, buildTemplateFromArgs, options, errorDecoder); this.httpExecutor = checkNotNull(httpExecutor, "httpExecutor for %s", target); this.incrementalDecoder = checkNotNull(incrementalDecoder, "incrementalDecoder for %s", target); @@ -185,7 +186,7 @@ static class SynchronousMethodHandler extends BaseMethodHandler { private final Decoder.TextStream decoder; private SynchronousMethodHandler(Target target, Client client, Provider retryer, Logger logger, - Logger.Level logLevel, MethodMetadata metadata, + Provider logLevel, MethodMetadata metadata, BuildTemplateFromArgs buildTemplateFromArgs, Options options, Decoder.TextStream decoder, ErrorDecoder errorDecoder) { super(target, client, retryer, logger, logLevel, metadata, buildTemplateFromArgs, options, errorDecoder); @@ -215,15 +216,14 @@ static abstract class BaseMethodHandler implements MethodHandler { protected final Client client; protected final Provider retryer; protected final Logger logger; - protected final Logger.Level logLevel; - + protected final Provider logLevel; protected final BuildTemplateFromArgs buildTemplateFromArgs; protected final Options options; protected final ErrorDecoder errorDecoder; private BaseMethodHandler(Target target, Client client, Provider retryer, Logger logger, - Logger.Level logLevel, MethodMetadata metadata, BuildTemplateFromArgs buildTemplateFromArgs, - Options options, ErrorDecoder errorDecoder) { + Provider 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); @@ -243,6 +243,9 @@ private BaseMethodHandler(Target target, Client client, Provider ret return executeAndDecode(argv, template); } catch (RetryableException e) { retryer.continueOrPropagate(e); + if (logLevel.get() != Logger.Level.NONE) { + logger.logRetry(metadata.configKey(), logLevel.get()); + } continue; } } @@ -251,8 +254,8 @@ private BaseMethodHandler(Target target, Client client, Provider ret 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; @@ -260,13 +263,16 @@ public Object executeAndDecode(Object[] argv, RequestTemplate template) throws T 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); @@ -274,12 +280,19 @@ public Object executeAndDecode(Object[] argv, RequestTemplate template) throws T 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()); } } + protected long elapsedTime(long start) { + return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); + } + protected Request targetRequest(RequestTemplate template) { return target.apply(new RequestTemplate(template)); } diff --git a/feign-core/src/test/java/feign/LoggerTest.java b/feign-core/src/test/java/feign/LoggerTest.java index d72d89cfa9..edd1c16883 100644 --- a/feign-core/src/test/java/feign/LoggerTest.java +++ b/feign-core/src/test/java/feign/LoggerTest.java @@ -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.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 @@ String login( } @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.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,7 +102,7 @@ public Object[][] createData() { @Test(dataProvider = "levelToOutput") public void levelEmits(final Logger.Level logLevel, List 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 { @@ -140,4 +139,221 @@ public void levelEmits(final Logger.Level logLevel, List expectedMessage server.shutdown(); } } + + @DataProvider(name = "levelToReadTimeoutOutput") + public Object[][] levelToReadTimeoutOutput() { + Object[][] data = new Object[4][2]; + data[0][0] = Logger.Level.NONE; + data[0][1] = Arrays.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; + } + + @Test(dataProvider = "levelToReadTimeoutOutput") + public void readTimeoutEmits(final Logger.Level logLevel, List 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() { + @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()); + + api.login("netflix", "denominator", "password"); + + fail(); + } catch (FeignException e) { + + assertMessagesMatch(expectedMessages); + + assertEquals(new String(server.takeRequest().getBody()), + "{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}"); + } finally { + server.shutdown(); + } + } + + @DataProvider(name = "levelToUnknownHostOutput") + public Object[][] levelToUnknownHostOutput() { + Object[][] data = new Object[4][2]; + data[0][0] = Logger.Level.NONE; + data[0][1] = Arrays.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; + } + + @Test(dataProvider = "levelToUnknownHostOutput") + public void unknownHostEmits(final Logger.Level logLevel, List 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() { + @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()); + + api.login("netflix", "denominator", "password"); + + fail(); + } catch (FeignException e) { + assertMessagesMatch(expectedMessages); + } + } + + + 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; + } + }; + } + + @Provides(type = SET) Encoder defaultEncoder() { + return new Encoder.Text() { + @Override public String encode(Object object) { + return object.toString(); + } + }; + } + + @Provides @Singleton Logger logger() { + return logger; + } + + @Provides @Singleton Logger.Level level() { + return Logger.Level.BASIC; + } + } + + try { + SendsStuff api = Feign.create(SendsStuff.class, "http://robofu.abc", new Module()); + + 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 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)); + } + } } From b3ba66bb325e8f3ee4279331c02264b3227de250 Mon Sep 17 00:00:00 2001 From: adriancole Date: Sun, 4 Aug 2013 08:13:33 -0700 Subject: [PATCH 076/179] bump to 5.0.0-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 5594a271c9..11d8403777 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=4.0.0-SNAPSHOT +version=5.0.0-SNAPSHOT From 08fd8888778f7cde4c9c75cf2dc8c23a3b5747c6 Mon Sep 17 00:00:00 2001 From: adriancole Date: Sun, 4 Aug 2013 08:29:29 -0700 Subject: [PATCH 077/179] update github example to use feign 4.0 observable --- examples/feign-example-cli/build.gradle | 4 +- .../java/feign/example/cli/GitHubExample.java | 76 ++++++++++++------- 2 files changed, 51 insertions(+), 29 deletions(-) diff --git a/examples/feign-example-cli/build.gradle b/examples/feign-example-cli/build.gradle index 55b0af2dea..ac174601de 100644 --- a/examples/feign-example-cli/build.gradle +++ b/examples/feign-example-cli/build.gradle @@ -1,8 +1,8 @@ apply plugin: 'java' dependencies { - compile 'com.netflix.feign:feign-core:3.0.0' - compile 'com.netflix.feign:feign-gson:3.0.0' + compile 'com.netflix.feign:feign-core:4.0.0' + compile 'com.netflix.feign:feign-gson:4.0.0' provided 'com.squareup.dagger:dagger-compiler:1.0.1' } diff --git a/examples/feign-example-cli/src/main/java/feign/example/cli/GitHubExample.java b/examples/feign-example-cli/src/main/java/feign/example/cli/GitHubExample.java index 3106e5116a..4120576a09 100644 --- a/examples/feign-example-cli/src/main/java/feign/example/cli/GitHubExample.java +++ b/examples/feign-example-cli/src/main/java/feign/example/cli/GitHubExample.java @@ -15,8 +15,12 @@ */ package feign.example.cli; +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 @@ interface GitHub { List 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 contributors); + Observable observable(@Named("owner") String owner, @Named("repo") String repo); } static class Contributor { @@ -44,7 +47,7 @@ static class Contributor { } 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 contributors = github.contributors("netflix", "feign"); @@ -52,36 +55,55 @@ public static void main(String... args) throws InterruptedException { 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 observable = github.observable("netflix", "feign"); - System.out.println("Now, let's do it as an incremental async task."); - IncrementalCallback task = new IncrementalCallback() { + 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 { - // 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(); + } } } From 653b16f0095e73560ad1663fcf275b62273b8dbf Mon Sep 17 00:00:00 2001 From: adriancole Date: Sun, 4 Aug 2013 12:48:30 -0700 Subject: [PATCH 078/179] renamed github example --- .../{feign-example-cli => feign-example-github}/build.gradle | 2 +- .../src/main/java/feign/example/github}/GitHubExample.java | 2 +- settings.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename examples/{feign-example-cli => feign-example-github}/build.gradle (95%) rename examples/{feign-example-cli/src/main/java/feign/example/cli => feign-example-github/src/main/java/feign/example/github}/GitHubExample.java (99%) diff --git a/examples/feign-example-cli/build.gradle b/examples/feign-example-github/build.gradle similarity index 95% rename from examples/feign-example-cli/build.gradle rename to examples/feign-example-github/build.gradle index ac174601de..94da115045 100644 --- a/examples/feign-example-cli/build.gradle +++ b/examples/feign-example-github/build.gradle @@ -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 diff --git a/examples/feign-example-cli/src/main/java/feign/example/cli/GitHubExample.java b/examples/feign-example-github/src/main/java/feign/example/github/GitHubExample.java similarity index 99% rename from examples/feign-example-cli/src/main/java/feign/example/cli/GitHubExample.java rename to examples/feign-example-github/src/main/java/feign/example/github/GitHubExample.java index 4120576a09..81e9b71f35 100644 --- a/examples/feign-example-cli/src/main/java/feign/example/cli/GitHubExample.java +++ b/examples/feign-example-github/src/main/java/feign/example/github/GitHubExample.java @@ -13,7 +13,7 @@ * 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; diff --git a/settings.gradle b/settings.gradle index f15a2c3970..c8929c5d43 100644 --- a/settings.gradle +++ b/settings.gradle @@ -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' From 3e65dcffbc604dff109c6864a88e23ba45d48e82 Mon Sep 17 00:00:00 2001 From: adriancole Date: Sun, 4 Aug 2013 12:51:31 -0700 Subject: [PATCH 079/179] close issue #37: add wikipedia example --- CHANGES.md | 3 + examples/feign-example-wikipedia/build.gradle | 49 ++++++ .../example/wikipedia/ResponseDecoder.java | 87 +++++++++++ .../example/wikipedia/WikipediaExample.java | 145 ++++++++++++++++++ settings.gradle | 2 +- 5 files changed, 285 insertions(+), 1 deletion(-) create mode 100644 examples/feign-example-wikipedia/build.gradle create mode 100644 examples/feign-example-wikipedia/src/main/java/feign/example/wikipedia/ResponseDecoder.java create mode 100644 examples/feign-example-wikipedia/src/main/java/feign/example/wikipedia/WikipediaExample.java diff --git a/CHANGES.md b/CHANGES.md index 7f537e9c0a..dbf8b5128d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,9 @@ * `Observer` replaces `IncrementalCallback` and is passed to `Observable.subscribe()`. * On `Subscription.unsubscribe()`, `Observer.onNext()` will stop being called. +### Version 3.2 +* Add wikipedia search example + ### Version 3.1 * Log when an http request is retried or a response fails due to an IOException. diff --git a/examples/feign-example-wikipedia/build.gradle b/examples/feign-example-wikipedia/build.gradle new file mode 100644 index 0000000000..dcd1c58eed --- /dev/null +++ b/examples/feign-example-wikipedia/build.gradle @@ -0,0 +1,49 @@ +apply plugin: 'java' + +dependencies { + compile 'com.netflix.feign:feign-core:4.0.0' + compile 'com.netflix.feign:feign-gson:4.0.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 +} diff --git a/examples/feign-example-wikipedia/src/main/java/feign/example/wikipedia/ResponseDecoder.java b/examples/feign-example-wikipedia/src/main/java/feign/example/wikipedia/ResponseDecoder.java new file mode 100644 index 0000000000..9cb54bba9a --- /dev/null +++ b/examples/feign-example-wikipedia/src/main/java/feign/example/wikipedia/ResponseDecoder.java @@ -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 implements Decoder.TextStream> { + + /** + * 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. + *

+ *
+ * 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}. + *

+ *

+   * "pages": {
+   *   "2576129": {
+   *     "pageid": 2576129,
+   *     "title": "Burchell's zebra",
+   * --snip--
+   * 
+ */ + 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 decode(Reader ireader, Type type) throws IOException { + WikipediaExample.Response pages = new WikipediaExample.Response(); + 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; + } +} diff --git a/examples/feign-example-wikipedia/src/main/java/feign/example/wikipedia/WikipediaExample.java b/examples/feign-example-wikipedia/src/main/java/feign/example/wikipedia/WikipediaExample.java new file mode 100644 index 0000000000..83151288fb --- /dev/null +++ b/examples/feign-example-wikipedia/src/main/java/feign/example/wikipedia/WikipediaExample.java @@ -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 search(@Named("search") String search); + + @RequestLine("GET /w/api.php?action=query&generator=search&prop=info&format=json&gsrsearch={search}&gsroffset={offset}") + Response resumeSearch(@Named("search") String search, @Named("offset") long offset); + } + + static class Page { + long id; + String title; + } + + public static class Response extends ArrayList { + /** + * 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 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 lazySearch(final Wikipedia wikipedia, final String query) { + final Response first = wikipedia.search(query); + if (first.nextOffset == null) + return first.iterator(); + return new Iterator() { + Iterator 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 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}. + */ + @Provides(type = SET) Decoder pagesDecoder() { + return new ResponseDecoder() { + + @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; + } + }; + } + } +} diff --git a/settings.gradle b/settings.gradle index c8929c5d43..bd5c8dd9ea 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ rootProject.name='feign' -include 'feign-core', 'feign-gson', 'feign-jaxrs', 'feign-ribbon', 'examples:feign-example-github' +include 'feign-core', 'feign-gson', 'feign-jaxrs', 'feign-ribbon', 'examples:feign-example-github', 'examples:feign-example-wikipedia' From dc59e64426b4b89f834c341c5f26b199cafb4f1d Mon Sep 17 00:00:00 2001 From: adriancole Date: Sun, 4 Aug 2013 13:14:43 -0700 Subject: [PATCH 080/179] fix issue #31: support @Path on type --- CHANGES.md | 1 + .../src/main/java/feign/jaxrs/JAXRSModule.java | 10 ++++++++++ .../test/java/feign/jaxrs/JAXRSContractTest.java | 14 ++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index dbf8b5128d..68ae1ba208 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ ### Version 3.2 * Add wikipedia search example +* Allow `@Path` on types in feign-jaxrs ### Version 3.1 * Log when an http request is retried or a response fails due to an IOException. diff --git a/feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java b/feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java index d224516969..db5d7883d3 100644 --- a/feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java +++ b/feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java @@ -45,6 +45,16 @@ 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) { + md.template().insert(0, path.value()); + } + return md; + } + @Override protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) { Class annotationType = methodAnnotation.annotationType(); diff --git a/feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java b/feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java index 0612e16066..cad033a84f 100644 --- a/feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java +++ b/feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java @@ -193,6 +193,20 @@ public void tooManyBodies() throws Exception { contract.parseAndValidatateMetadata(BodyParams.class.getDeclaredMethod("tooMany", List.class, List.class)); } + @Path("/base") + interface PathOnType { + @GET Response base(); + + @GET @Path("/specific") Response get(); + } + + @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"); + } + interface WithURIParam { @GET @Path("/{1}/{2}") Response uriParam(@PathParam("1") String one, URI endpoint, @PathParam("2") String two); } From f1af6001c0849b0dfa660fd44751e6b80a85cc84 Mon Sep 17 00:00:00 2001 From: adriancole Date: Tue, 6 Aug 2013 11:55:45 -0400 Subject: [PATCH 081/179] update to dagger 1.1, as bump test deps --- CHANGES.md | 1 + build.gradle | 22 +-- feign-core/src/test/java/feign/FeignTest.java | 134 +++++++------- .../src/test/java/feign/LoggerTest.java | 167 +++++++----------- .../test/java/feign/gson/GsonModuleTest.java | 52 +++--- 5 files changed, 178 insertions(+), 198 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 68ae1ba208..7f57b4cef0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ * On `Subscription.unsubscribe()`, `Observer.onNext()` will stop being called. ### Version 3.2 +* update to dagger 1.1 * Add wikipedia search example * Allow `@Path` on types in feign-jaxrs diff --git a/build.gradle b/build.gradle index b47bdfa428..df7ad91b4e 100644 --- a/build.gradle +++ b/build.gradle @@ -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,8 +55,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' } } @@ -70,12 +70,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 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 +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' } } diff --git a/feign-core/src/test/java/feign/FeignTest.java b/feign-core/src/test/java/feign/FeignTest.java index a114c5473f..fba37b919f 100644 --- a/feign-core/src/test/java/feign/FeignTest.java +++ b/feign-core/src/test/java/feign/FeignTest.java @@ -125,7 +125,7 @@ public void observableVoid() throws IOException, InterruptedException { 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(); @@ -159,7 +159,7 @@ public void observableResponse() throws IOException, InterruptedException { 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(); @@ -193,7 +193,7 @@ public void incrementString() throws IOException, InterruptedException { 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(); @@ -228,7 +228,7 @@ public void multipleObservers() throws IOException, InterruptedException { 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); @@ -265,7 +265,7 @@ public void postTemplateParamsResolve() throws IOException, InterruptedException 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()), @@ -282,7 +282,7 @@ public void postFormParams() throws IOException, InterruptedException { 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()), @@ -299,7 +299,7 @@ public void postBodyParam() throws IOException, InterruptedException { 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]"); @@ -314,29 +314,32 @@ public void postBodyParam() throws IOException, InterruptedException { 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 { @@ -351,7 +354,7 @@ public Exception decode(String methodKey, Response response) { 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 +365,26 @@ public Exception decode(String methodKey, Response response) { } } + @dagger.Module(overrides = true, library = true, includes = TestInterface.Module.class) + static class DecodeFail { + @Provides(type = SET) Decoder decoder() { + return new Decoder.TextStream() { + @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() { - @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,6 +393,21 @@ public String decode(Reader reader, Type type) throws IOException { } } + @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. */ @@ -397,20 +418,8 @@ public void retryableExceptionInDecoder() throws IOException, InterruptedExcepti 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,6 +428,18 @@ public String decode(Reader reader, Type type) throws RetryableException, IOExce } } + @dagger.Module(overrides = true, library = true, includes = TestInterface.Module.class) + static class IOEOnDecode { + @Provides(type = SET) Decoder decoder() { + return new Decoder.TextStream() { + @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(); @@ -426,17 +447,8 @@ public void doesntRetryAfterResponseIsSent() throws IOException, InterruptedExce server.play(); try { - @dagger.Module(overrides = true, includes = TestInterface.Module.class) class Overrides { - @Provides(type = SET) Decoder decoder() { - return new Decoder.TextStream() { - @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 { @@ -459,7 +471,7 @@ static class TrustSSLSockets { 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 { @@ -475,7 +487,7 @@ static class TrustSSLSockets { 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); diff --git a/feign-core/src/test/java/feign/LoggerTest.java b/feign-core/src/test/java/feign/LoggerTest.java index edd1c16883..a7a715ed2b 100644 --- a/feign-core/src/test/java/feign/LoggerTest.java +++ b/feign-core/src/test/java/feign/LoggerTest.java @@ -105,26 +105,9 @@ public void levelEmits(final Logger.Level logLevel, List expectedMessage 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() { - @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 void levelEmits(final Logger.Level logLevel, List expectedMessage } } + 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() { + @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 Object[][] levelToReadTimeoutOutput() { 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 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() { - @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 Object[][] levelToUnknownHostOutput() { 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 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() { - @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 void unknownHostEmits(final Logger.Level logLevel, List expectedM } } + @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() { - @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"); diff --git a/feign-gson/src/test/java/feign/gson/GsonModuleTest.java b/feign-gson/src/test/java/feign/gson/GsonModuleTest.java index 6f7ddb5519..983c58ffcf 100644 --- a/feign-gson/src/test/java/feign/gson/GsonModuleTest.java +++ b/feign-gson/src/test/java/feign/gson/GsonModuleTest.java @@ -40,15 +40,15 @@ @Test public class GsonModuleTest { + @Module(includes = GsonModule.class, library = true, injects = EncodersAndDecoders.class) + static class EncodersAndDecoders { + @Inject Set encoders; + @Inject Set decoders; + @Inject Set incrementalDecoders; + } @Test public void providesEncoderDecoderAndIncrementalDecoder() throws Exception { - @Module(includes = GsonModule.class, injects = SetBindings.class) class SetBindings { - @Inject Set encoders; - @Inject Set decoders; - @Inject Set 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 { 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 encoders; - } + @Module(includes = GsonModule.class, library = true, injects = Encoders.class) + static class Encoders { + @Inject Set encoders; + } - SetBindings bindings = new SetBindings(); + @Test public void encodesMapObjectNumericalValuesAsInteger() throws Exception { + Encoders bindings = new Encoders(); ObjectGraph.create(bindings).inject(bindings); Map map = new LinkedHashMap(); @@ -77,11 +78,8 @@ public class GsonModuleTest { } @Test public void encodesFormParams() throws Exception { - @Module(includes = GsonModule.class, injects = SetBindings.class) class SetBindings { - @Inject Set encoders; - } - SetBindings bindings = new SetBindings(); + Encoders bindings = new Encoders(); ObjectGraph.create(bindings).inject(bindings); Map form = new LinkedHashMap(); @@ -116,12 +114,13 @@ static class Zone extends LinkedHashMap { private static final long serialVersionUID = 1L; } - @Test public void decodes() throws Exception { - @Module(includes = GsonModule.class, injects = SetBindings.class) class SetBindings { - @Inject Set decoders; - } + @Module(includes = GsonModule.class, library = true, injects = Decoders.class) + static class Decoders { + @Inject Set decoders; + } - SetBindings bindings = new SetBindings(); + @Test public void decodes() throws Exception { + Decoders bindings = new Decoders(); ObjectGraph.create(bindings).inject(bindings); List zones = new LinkedList(); @@ -133,12 +132,13 @@ static class Zone extends LinkedHashMap { }.getType()), zones); } - @Test public void decodesIncrementally() throws Exception { - @Module(includes = GsonModule.class, injects = SetBindings.class) class SetBindings { - @Inject Set decoders; - } + @Module(includes = GsonModule.class, library = true, injects = IncrementalDecoders.class) + static class IncrementalDecoders { + @Inject Set decoders; + } - SetBindings bindings = new SetBindings(); + @Test public void decodesIncrementally() throws Exception { + IncrementalDecoders bindings = new IncrementalDecoders(); ObjectGraph.create(bindings).inject(bindings); final List zones = new LinkedList(); From 6195a81aa25c75ed31bcccb157fc14f8b8649b20 Mon Sep 17 00:00:00 2001 From: adriancole Date: Tue, 6 Aug 2013 14:33:08 -0400 Subject: [PATCH 082/179] 4.1 --- CHANGES.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7f57b4cef0..cccc683fc2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,14 +1,14 @@ +### Version 4.1/3.2 +* update to dagger 1.1 +* Add wikipedia search example +* Allow `@Path` on types in feign-jaxrs + ### Version 4.0 * Support RxJava-style Observers. * Return type can be `Observable` for an async equiv of `Iterable`. * `Observer` replaces `IncrementalCallback` and is passed to `Observable.subscribe()`. * On `Subscription.unsubscribe()`, `Observer.onNext()` will stop being called. -### Version 3.2 -* update to dagger 1.1 -* Add wikipedia search example -* Allow `@Path` on types in feign-jaxrs - ### Version 3.1 * Log when an http request is retried or a response fails due to an IOException. From 353ead1863ac42c83c4f3a39d243290ce0118cd7 Mon Sep 17 00:00:00 2001 From: adriancole Date: Tue, 6 Aug 2013 17:05:59 -0400 Subject: [PATCH 083/179] updated examples to 4.1 --- examples/feign-example-github/build.gradle | 6 +++--- examples/feign-example-wikipedia/build.gradle | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/feign-example-github/build.gradle b/examples/feign-example-github/build.gradle index 94da115045..3ca897e3bf 100644 --- a/examples/feign-example-github/build.gradle +++ b/examples/feign-example-github/build.gradle @@ -1,9 +1,9 @@ apply plugin: 'java' dependencies { - compile 'com.netflix.feign:feign-core:4.0.0' - compile 'com.netflix.feign:feign-gson:4.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 diff --git a/examples/feign-example-wikipedia/build.gradle b/examples/feign-example-wikipedia/build.gradle index dcd1c58eed..6d9a64d0b8 100644 --- a/examples/feign-example-wikipedia/build.gradle +++ b/examples/feign-example-wikipedia/build.gradle @@ -1,9 +1,9 @@ apply plugin: 'java' dependencies { - compile 'com.netflix.feign:feign-core:4.0.0' - compile 'com.netflix.feign:feign-gson:4.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 From 8a4d5dad7eacffce3ffdc59fb3937ab48443e774 Mon Sep 17 00:00:00 2001 From: adriancole Date: Wed, 14 Aug 2013 09:44:54 -0700 Subject: [PATCH 084/179] issue #44: ensure jax-rs annotations are processes from POV of server interfaces --- CHANGES.md | 3 + feign-jaxrs/README.md | 37 ++++++ .../main/java/feign/jaxrs/JAXRSModule.java | 45 ++++--- .../java/feign/jaxrs/JAXRSContractTest.java | 110 ++++++++++++++---- 4 files changed, 151 insertions(+), 44 deletions(-) create mode 100644 feign-jaxrs/README.md diff --git a/CHANGES.md b/CHANGES.md index cccc683fc2..5e5c24ca8b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,6 @@ +### Version 4.2/3.3 +* Document and enforce JAX-RS annotation processing from server POV + ### Version 4.1/3.2 * update to dagger 1.1 * Add wikipedia search example diff --git a/feign-jaxrs/README.md b/feign-jaxrs/README.md new file mode 100644 index 0000000000..5f53d92f94 --- /dev/null +++ b/feign-jaxrs/README.md @@ -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. +#### `@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>.encode()`. diff --git a/feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java b/feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java index db5d7883d3..dc84c8e2d0 100644 --- a/feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java +++ b/feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java @@ -33,7 +33,12 @@ import java.util.Collection; import static feign.Util.checkState; +import static feign.Util.emptyToNull; +/** + * Please refer to the + * Feign JAX-RS README. + */ @dagger.Module(library = true, overrides = true) public final class JAXRSModule { static final String ACCEPT = "Accept"; @@ -50,7 +55,9 @@ public MethodMetadata parseAndValidatateMetadata(Method method) { MethodMetadata md = super.parseAndValidatateMetadata(method); Path path = method.getDeclaringClass().getAnnotation(Path.class); if (path != null) { - md.template().insert(0, path.value()); + 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; } @@ -64,19 +71,20 @@ protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodA "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); } } @@ -87,22 +95,26 @@ protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[ Class 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 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 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; @@ -111,17 +123,4 @@ protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[ 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(); - } } diff --git a/feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java b/feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java index cad033a84f..1669e3698c 100644 --- a/feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java +++ b/feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java @@ -18,13 +18,13 @@ 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.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 @@ interface WithQueryParamsInPath { } } - interface BodyWithoutParameters { - @POST @Produces(APPLICATION_XML) @Body("") 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 public void bodyWithoutParameters() throws Exception { - MethodMetadata md = contract.parseAndValidatateMetadata(BodyWithoutParameters.class.getDeclaredMethod("post")); - assertEquals(md.template().body(), ""); - assertFalse(md.template().bodyTemplate() != null); - assertTrue(md.formParams().isEmpty()); - assertTrue(md.indexToName().isEmpty()); + @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 producesAddsContentTypeHeader() throws Exception { - MethodMetadata md = contract.parseAndValidatateMetadata(BodyWithoutParameters.class.getDeclaredMethod("post")); - assertEquals(md.template().headers().get(CONTENT_TYPE), ImmutableSet.of(APPLICATION_XML)); + @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(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "Consumes.value\\(\\) was empty on method consumesNada") + public void consumesNada() throws Exception { + contract.parseAndValidatateMetadata(ProducesAndConsumes.class.getDeclaredMethod("consumesNada")); + } + + @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,11 +221,23 @@ public void tooManyBodies() throws Exception { contract.parseAndValidatateMetadata(BodyParams.class.getDeclaredMethod("tooMany", List.class, List.class)); } - @Path("/base") - interface PathOnType { + @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 { @@ -207,6 +247,16 @@ interface PathOnType { 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); } @@ -229,6 +279,8 @@ interface WithPathAndQueryParams { @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 { @@ -246,29 +298,40 @@ Response recordsByNameAndType(@PathParam("domainId") int id, @QueryParam("name") 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 { @@ -278,6 +341,11 @@ interface HeaderParams { 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> valid(); From 25d47445dc57ce3ad0a12c0f14c5eb67eb9d4c7b Mon Sep 17 00:00:00 2001 From: adriancole Date: Wed, 14 Aug 2013 12:25:41 -0700 Subject: [PATCH 085/179] Skip query template parameters when corresponding java arg is null --- CHANGES.md | 1 + .../src/main/java/feign/RequestTemplate.java | 37 +++++++++++++++++-- .../test/java/feign/RequestTemplateTest.java | 32 +++++++++++++++- feign-jaxrs/README.md | 2 +- 4 files changed, 66 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 5e5c24ca8b..6f156f77fa 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ ### 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 diff --git a/feign-core/src/main/java/feign/RequestTemplate.java b/feign-core/src/main/java/feign/RequestTemplate.java index 5b2b9a46e6..f3081a1927 100644 --- a/feign-core/src/main/java/feign/RequestTemplate.java +++ b/feign-core/src/main/java/feign/RequestTemplate.java @@ -23,6 +23,7 @@ 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 RequestTemplate resolve(Map unencoded) { for (Entry 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 @@ private static void putKV(String stringToParse, Map> 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 unencoded) { + Iterator>> iterator = queries.entrySet().iterator(); + while (iterator.hasNext()) { + Entry> entry = iterator.next(); + if (entry.getValue() == null) { + continue; + } + Collection values = new ArrayList(); + 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 String queryLine() { return queryBuilder.insert(0, '?').toString(); } - private static final long serialVersionUID = 1L; } diff --git a/feign-core/src/test/java/feign/RequestTemplateTest.java b/feign-core/src/test/java/feign/RequestTemplateTest.java index 173ce535e7..ffc13e9deb 100644 --- a/feign-core/src/test/java/feign/RequestTemplateTest.java +++ b/feign-core/src/test/java/feign/RequestTemplateTest.java @@ -18,7 +18,6 @@ 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 { + "\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.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.builder()// + .put("domainId", 1001)// + .build() + ); + + assertEquals(template.toString(), ""// + + "GET /domains/1001/records HTTP/1.1\n"); + } } diff --git a/feign-jaxrs/README.md b/feign-jaxrs/README.md index 5f53d92f94..5026c7ac00 100644 --- a/feign-jaxrs/README.md +++ b/feign-jaxrs/README.md @@ -30,7 +30,7 @@ Adds the first value as the `Content-Type` header. #### `@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. +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` From 05dcebb1eee6a3471542070466ffed5c19f00248 Mon Sep 17 00:00:00 2001 From: adriancole Date: Sat, 17 Aug 2013 15:16:41 -0700 Subject: [PATCH 086/179] flattened project structure so that eclipse gradle plugin will work --- .../src/main/java/feign/Body.java | 0 .../src/main/java/feign/Client.java | 0 .../src/main/java/feign/Contract.java | 0 .../src/main/java/feign/Feign.java | 0 .../src/main/java/feign/FeignException.java | 0 .../src/main/java/feign/Headers.java | 0 .../src/main/java/feign/Logger.java | 0 .../src/main/java/feign/MethodHandler.java | 0 .../src/main/java/feign/MethodMetadata.java | 0 .../src/main/java/feign/Observable.java | 0 .../src/main/java/feign/Observer.java | 0 .../src/main/java/feign/ReflectiveFeign.java | 0 .../src/main/java/feign/Request.java | 0 .../src/main/java/feign/RequestLine.java | 0 .../src/main/java/feign/RequestTemplate.java | 0 .../src/main/java/feign/Response.java | 0 .../main/java/feign/RetryableException.java | 0 .../src/main/java/feign/Retryer.java | 0 .../src/main/java/feign/Subscription.java | 0 .../src/main/java/feign/Target.java | 0 .../src/main/java/feign/Types.java | 0 .../src/main/java/feign/Util.java | 0 .../java/feign/codec/DecodeException.java | 0 .../src/main/java/feign/codec/Decoder.java | 0 .../src/main/java/feign/codec/Decoders.java | 0 .../java/feign/codec/EncodeException.java | 0 .../src/main/java/feign/codec/Encoder.java | 0 .../main/java/feign/codec/ErrorDecoder.java | 0 .../java/feign/codec/IncrementalDecoder.java | 0 .../src/main/java/feign/codec/SAXDecoder.java | 0 .../main/java/feign/codec/StringDecoder.java | 0 .../feign/codec/StringIncrementalDecoder.java | 0 .../test/java/feign/DefaultContractTest.java | 0 .../test/java/feign/DefaultRetryerTest.java | 0 .../src/test/java/feign/FeignTest.java | 0 .../src/test/java/feign/LoggerTest.java | 0 .../test/java/feign/RequestTemplateTest.java | 0 .../java/feign/TrustingSSLSocketFactory.java | 0 .../src/test/java/feign/UtilTest.java | 0 .../feign/codec/DefaultErrorDecoderTest.java | 0 .../feign/codec/RetryAfterDecoderTest.java | 0 .../feign/examples/AWSSignatureVersion4.java | 0 .../java/feign/examples/GitHubExample.java | 0 .../test/java/feign/examples/IAMExample.java | 0 .../build.gradle | 0 .../feign/example/github/GitHubExample.java | 0 .../build.gradle | 0 .../example/wikipedia/ResponseDecoder.java | 0 .../example/wikipedia/WikipediaExample.java | 0 .../java/feign/jaxrs/examples/IAMExample.java | 76 ------------------- {feign-gson => gson}/README.md | 0 .../src/main/java/feign/gson/GsonModule.java | 0 .../test/java/feign/gson/GsonModuleTest.java | 0 {feign-jaxrs => jaxrs}/README.md | 0 .../main/java/feign/jaxrs/JAXRSModule.java | 0 .../java/feign/jaxrs/JAXRSContractTest.java | 0 .../feign/jaxrs/examples/GitHubExample.java | 0 {feign-ribbon => ribbon}/README.md | 0 .../src/main/java/feign/ribbon/LBClient.java | 0 .../feign/ribbon/LoadBalancingTarget.java | 0 .../main/java/feign/ribbon/RibbonModule.java | 0 .../feign/ribbon/LoadBalancingTargetTest.java | 0 .../java/feign/ribbon/RibbonClientTest.java | 0 settings.gradle | 6 +- 64 files changed, 5 insertions(+), 77 deletions(-) rename {feign-core => core}/src/main/java/feign/Body.java (100%) rename {feign-core => core}/src/main/java/feign/Client.java (100%) rename {feign-core => core}/src/main/java/feign/Contract.java (100%) rename {feign-core => core}/src/main/java/feign/Feign.java (100%) rename {feign-core => core}/src/main/java/feign/FeignException.java (100%) rename {feign-core => core}/src/main/java/feign/Headers.java (100%) rename {feign-core => core}/src/main/java/feign/Logger.java (100%) rename {feign-core => core}/src/main/java/feign/MethodHandler.java (100%) rename {feign-core => core}/src/main/java/feign/MethodMetadata.java (100%) rename {feign-core => core}/src/main/java/feign/Observable.java (100%) rename {feign-core => core}/src/main/java/feign/Observer.java (100%) rename {feign-core => core}/src/main/java/feign/ReflectiveFeign.java (100%) rename {feign-core => core}/src/main/java/feign/Request.java (100%) rename {feign-core => core}/src/main/java/feign/RequestLine.java (100%) rename {feign-core => core}/src/main/java/feign/RequestTemplate.java (100%) rename {feign-core => core}/src/main/java/feign/Response.java (100%) rename {feign-core => core}/src/main/java/feign/RetryableException.java (100%) rename {feign-core => core}/src/main/java/feign/Retryer.java (100%) rename {feign-core => core}/src/main/java/feign/Subscription.java (100%) rename {feign-core => core}/src/main/java/feign/Target.java (100%) rename {feign-core => core}/src/main/java/feign/Types.java (100%) rename {feign-core => core}/src/main/java/feign/Util.java (100%) rename {feign-core => core}/src/main/java/feign/codec/DecodeException.java (100%) rename {feign-core => core}/src/main/java/feign/codec/Decoder.java (100%) rename {feign-core => core}/src/main/java/feign/codec/Decoders.java (100%) rename {feign-core => core}/src/main/java/feign/codec/EncodeException.java (100%) rename {feign-core => core}/src/main/java/feign/codec/Encoder.java (100%) rename {feign-core => core}/src/main/java/feign/codec/ErrorDecoder.java (100%) rename {feign-core => core}/src/main/java/feign/codec/IncrementalDecoder.java (100%) rename {feign-core => core}/src/main/java/feign/codec/SAXDecoder.java (100%) rename {feign-core => core}/src/main/java/feign/codec/StringDecoder.java (100%) rename {feign-core => core}/src/main/java/feign/codec/StringIncrementalDecoder.java (100%) rename {feign-core => core}/src/test/java/feign/DefaultContractTest.java (100%) rename {feign-core => core}/src/test/java/feign/DefaultRetryerTest.java (100%) rename {feign-core => core}/src/test/java/feign/FeignTest.java (100%) rename {feign-core => core}/src/test/java/feign/LoggerTest.java (100%) rename {feign-core => core}/src/test/java/feign/RequestTemplateTest.java (100%) rename {feign-core => core}/src/test/java/feign/TrustingSSLSocketFactory.java (100%) rename {feign-core => core}/src/test/java/feign/UtilTest.java (100%) rename {feign-core => core}/src/test/java/feign/codec/DefaultErrorDecoderTest.java (100%) rename {feign-core => core}/src/test/java/feign/codec/RetryAfterDecoderTest.java (100%) rename {feign-core => core}/src/test/java/feign/examples/AWSSignatureVersion4.java (100%) rename {feign-core => core}/src/test/java/feign/examples/GitHubExample.java (100%) rename {feign-core => core}/src/test/java/feign/examples/IAMExample.java (100%) rename {examples/feign-example-github => example-github}/build.gradle (100%) rename {examples/feign-example-github => example-github}/src/main/java/feign/example/github/GitHubExample.java (100%) rename {examples/feign-example-wikipedia => example-wikipedia}/build.gradle (100%) rename {examples/feign-example-wikipedia => example-wikipedia}/src/main/java/feign/example/wikipedia/ResponseDecoder.java (100%) rename {examples/feign-example-wikipedia => example-wikipedia}/src/main/java/feign/example/wikipedia/WikipediaExample.java (100%) delete mode 100644 feign-jaxrs/src/test/java/feign/jaxrs/examples/IAMExample.java rename {feign-gson => gson}/README.md (100%) rename {feign-gson => gson}/src/main/java/feign/gson/GsonModule.java (100%) rename {feign-gson => gson}/src/test/java/feign/gson/GsonModuleTest.java (100%) rename {feign-jaxrs => jaxrs}/README.md (100%) rename {feign-jaxrs => jaxrs}/src/main/java/feign/jaxrs/JAXRSModule.java (100%) rename {feign-jaxrs => jaxrs}/src/test/java/feign/jaxrs/JAXRSContractTest.java (100%) rename {feign-jaxrs => jaxrs}/src/test/java/feign/jaxrs/examples/GitHubExample.java (100%) rename {feign-ribbon => ribbon}/README.md (100%) rename {feign-ribbon => ribbon}/src/main/java/feign/ribbon/LBClient.java (100%) rename {feign-ribbon => ribbon}/src/main/java/feign/ribbon/LoadBalancingTarget.java (100%) rename {feign-ribbon => ribbon}/src/main/java/feign/ribbon/RibbonModule.java (100%) rename {feign-ribbon => ribbon}/src/test/java/feign/ribbon/LoadBalancingTargetTest.java (100%) rename {feign-ribbon => ribbon}/src/test/java/feign/ribbon/RibbonClientTest.java (100%) diff --git a/feign-core/src/main/java/feign/Body.java b/core/src/main/java/feign/Body.java similarity index 100% rename from feign-core/src/main/java/feign/Body.java rename to core/src/main/java/feign/Body.java diff --git a/feign-core/src/main/java/feign/Client.java b/core/src/main/java/feign/Client.java similarity index 100% rename from feign-core/src/main/java/feign/Client.java rename to core/src/main/java/feign/Client.java diff --git a/feign-core/src/main/java/feign/Contract.java b/core/src/main/java/feign/Contract.java similarity index 100% rename from feign-core/src/main/java/feign/Contract.java rename to core/src/main/java/feign/Contract.java diff --git a/feign-core/src/main/java/feign/Feign.java b/core/src/main/java/feign/Feign.java similarity index 100% rename from feign-core/src/main/java/feign/Feign.java rename to core/src/main/java/feign/Feign.java diff --git a/feign-core/src/main/java/feign/FeignException.java b/core/src/main/java/feign/FeignException.java similarity index 100% rename from feign-core/src/main/java/feign/FeignException.java rename to core/src/main/java/feign/FeignException.java diff --git a/feign-core/src/main/java/feign/Headers.java b/core/src/main/java/feign/Headers.java similarity index 100% rename from feign-core/src/main/java/feign/Headers.java rename to core/src/main/java/feign/Headers.java diff --git a/feign-core/src/main/java/feign/Logger.java b/core/src/main/java/feign/Logger.java similarity index 100% rename from feign-core/src/main/java/feign/Logger.java rename to core/src/main/java/feign/Logger.java diff --git a/feign-core/src/main/java/feign/MethodHandler.java b/core/src/main/java/feign/MethodHandler.java similarity index 100% rename from feign-core/src/main/java/feign/MethodHandler.java rename to core/src/main/java/feign/MethodHandler.java diff --git a/feign-core/src/main/java/feign/MethodMetadata.java b/core/src/main/java/feign/MethodMetadata.java similarity index 100% rename from feign-core/src/main/java/feign/MethodMetadata.java rename to core/src/main/java/feign/MethodMetadata.java diff --git a/feign-core/src/main/java/feign/Observable.java b/core/src/main/java/feign/Observable.java similarity index 100% rename from feign-core/src/main/java/feign/Observable.java rename to core/src/main/java/feign/Observable.java diff --git a/feign-core/src/main/java/feign/Observer.java b/core/src/main/java/feign/Observer.java similarity index 100% rename from feign-core/src/main/java/feign/Observer.java rename to core/src/main/java/feign/Observer.java diff --git a/feign-core/src/main/java/feign/ReflectiveFeign.java b/core/src/main/java/feign/ReflectiveFeign.java similarity index 100% rename from feign-core/src/main/java/feign/ReflectiveFeign.java rename to core/src/main/java/feign/ReflectiveFeign.java diff --git a/feign-core/src/main/java/feign/Request.java b/core/src/main/java/feign/Request.java similarity index 100% rename from feign-core/src/main/java/feign/Request.java rename to core/src/main/java/feign/Request.java diff --git a/feign-core/src/main/java/feign/RequestLine.java b/core/src/main/java/feign/RequestLine.java similarity index 100% rename from feign-core/src/main/java/feign/RequestLine.java rename to core/src/main/java/feign/RequestLine.java diff --git a/feign-core/src/main/java/feign/RequestTemplate.java b/core/src/main/java/feign/RequestTemplate.java similarity index 100% rename from feign-core/src/main/java/feign/RequestTemplate.java rename to core/src/main/java/feign/RequestTemplate.java diff --git a/feign-core/src/main/java/feign/Response.java b/core/src/main/java/feign/Response.java similarity index 100% rename from feign-core/src/main/java/feign/Response.java rename to core/src/main/java/feign/Response.java diff --git a/feign-core/src/main/java/feign/RetryableException.java b/core/src/main/java/feign/RetryableException.java similarity index 100% rename from feign-core/src/main/java/feign/RetryableException.java rename to core/src/main/java/feign/RetryableException.java diff --git a/feign-core/src/main/java/feign/Retryer.java b/core/src/main/java/feign/Retryer.java similarity index 100% rename from feign-core/src/main/java/feign/Retryer.java rename to core/src/main/java/feign/Retryer.java diff --git a/feign-core/src/main/java/feign/Subscription.java b/core/src/main/java/feign/Subscription.java similarity index 100% rename from feign-core/src/main/java/feign/Subscription.java rename to core/src/main/java/feign/Subscription.java diff --git a/feign-core/src/main/java/feign/Target.java b/core/src/main/java/feign/Target.java similarity index 100% rename from feign-core/src/main/java/feign/Target.java rename to core/src/main/java/feign/Target.java diff --git a/feign-core/src/main/java/feign/Types.java b/core/src/main/java/feign/Types.java similarity index 100% rename from feign-core/src/main/java/feign/Types.java rename to core/src/main/java/feign/Types.java diff --git a/feign-core/src/main/java/feign/Util.java b/core/src/main/java/feign/Util.java similarity index 100% rename from feign-core/src/main/java/feign/Util.java rename to core/src/main/java/feign/Util.java diff --git a/feign-core/src/main/java/feign/codec/DecodeException.java b/core/src/main/java/feign/codec/DecodeException.java similarity index 100% rename from feign-core/src/main/java/feign/codec/DecodeException.java rename to core/src/main/java/feign/codec/DecodeException.java diff --git a/feign-core/src/main/java/feign/codec/Decoder.java b/core/src/main/java/feign/codec/Decoder.java similarity index 100% rename from feign-core/src/main/java/feign/codec/Decoder.java rename to core/src/main/java/feign/codec/Decoder.java diff --git a/feign-core/src/main/java/feign/codec/Decoders.java b/core/src/main/java/feign/codec/Decoders.java similarity index 100% rename from feign-core/src/main/java/feign/codec/Decoders.java rename to core/src/main/java/feign/codec/Decoders.java diff --git a/feign-core/src/main/java/feign/codec/EncodeException.java b/core/src/main/java/feign/codec/EncodeException.java similarity index 100% rename from feign-core/src/main/java/feign/codec/EncodeException.java rename to core/src/main/java/feign/codec/EncodeException.java diff --git a/feign-core/src/main/java/feign/codec/Encoder.java b/core/src/main/java/feign/codec/Encoder.java similarity index 100% rename from feign-core/src/main/java/feign/codec/Encoder.java rename to core/src/main/java/feign/codec/Encoder.java diff --git a/feign-core/src/main/java/feign/codec/ErrorDecoder.java b/core/src/main/java/feign/codec/ErrorDecoder.java similarity index 100% rename from feign-core/src/main/java/feign/codec/ErrorDecoder.java rename to core/src/main/java/feign/codec/ErrorDecoder.java diff --git a/feign-core/src/main/java/feign/codec/IncrementalDecoder.java b/core/src/main/java/feign/codec/IncrementalDecoder.java similarity index 100% rename from feign-core/src/main/java/feign/codec/IncrementalDecoder.java rename to core/src/main/java/feign/codec/IncrementalDecoder.java diff --git a/feign-core/src/main/java/feign/codec/SAXDecoder.java b/core/src/main/java/feign/codec/SAXDecoder.java similarity index 100% rename from feign-core/src/main/java/feign/codec/SAXDecoder.java rename to core/src/main/java/feign/codec/SAXDecoder.java diff --git a/feign-core/src/main/java/feign/codec/StringDecoder.java b/core/src/main/java/feign/codec/StringDecoder.java similarity index 100% rename from feign-core/src/main/java/feign/codec/StringDecoder.java rename to core/src/main/java/feign/codec/StringDecoder.java diff --git a/feign-core/src/main/java/feign/codec/StringIncrementalDecoder.java b/core/src/main/java/feign/codec/StringIncrementalDecoder.java similarity index 100% rename from feign-core/src/main/java/feign/codec/StringIncrementalDecoder.java rename to core/src/main/java/feign/codec/StringIncrementalDecoder.java diff --git a/feign-core/src/test/java/feign/DefaultContractTest.java b/core/src/test/java/feign/DefaultContractTest.java similarity index 100% rename from feign-core/src/test/java/feign/DefaultContractTest.java rename to core/src/test/java/feign/DefaultContractTest.java diff --git a/feign-core/src/test/java/feign/DefaultRetryerTest.java b/core/src/test/java/feign/DefaultRetryerTest.java similarity index 100% rename from feign-core/src/test/java/feign/DefaultRetryerTest.java rename to core/src/test/java/feign/DefaultRetryerTest.java diff --git a/feign-core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java similarity index 100% rename from feign-core/src/test/java/feign/FeignTest.java rename to core/src/test/java/feign/FeignTest.java diff --git a/feign-core/src/test/java/feign/LoggerTest.java b/core/src/test/java/feign/LoggerTest.java similarity index 100% rename from feign-core/src/test/java/feign/LoggerTest.java rename to core/src/test/java/feign/LoggerTest.java diff --git a/feign-core/src/test/java/feign/RequestTemplateTest.java b/core/src/test/java/feign/RequestTemplateTest.java similarity index 100% rename from feign-core/src/test/java/feign/RequestTemplateTest.java rename to core/src/test/java/feign/RequestTemplateTest.java diff --git a/feign-core/src/test/java/feign/TrustingSSLSocketFactory.java b/core/src/test/java/feign/TrustingSSLSocketFactory.java similarity index 100% rename from feign-core/src/test/java/feign/TrustingSSLSocketFactory.java rename to core/src/test/java/feign/TrustingSSLSocketFactory.java diff --git a/feign-core/src/test/java/feign/UtilTest.java b/core/src/test/java/feign/UtilTest.java similarity index 100% rename from feign-core/src/test/java/feign/UtilTest.java rename to core/src/test/java/feign/UtilTest.java diff --git a/feign-core/src/test/java/feign/codec/DefaultErrorDecoderTest.java b/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java similarity index 100% rename from feign-core/src/test/java/feign/codec/DefaultErrorDecoderTest.java rename to core/src/test/java/feign/codec/DefaultErrorDecoderTest.java diff --git a/feign-core/src/test/java/feign/codec/RetryAfterDecoderTest.java b/core/src/test/java/feign/codec/RetryAfterDecoderTest.java similarity index 100% rename from feign-core/src/test/java/feign/codec/RetryAfterDecoderTest.java rename to core/src/test/java/feign/codec/RetryAfterDecoderTest.java diff --git a/feign-core/src/test/java/feign/examples/AWSSignatureVersion4.java b/core/src/test/java/feign/examples/AWSSignatureVersion4.java similarity index 100% rename from feign-core/src/test/java/feign/examples/AWSSignatureVersion4.java rename to core/src/test/java/feign/examples/AWSSignatureVersion4.java diff --git a/feign-core/src/test/java/feign/examples/GitHubExample.java b/core/src/test/java/feign/examples/GitHubExample.java similarity index 100% rename from feign-core/src/test/java/feign/examples/GitHubExample.java rename to core/src/test/java/feign/examples/GitHubExample.java diff --git a/feign-core/src/test/java/feign/examples/IAMExample.java b/core/src/test/java/feign/examples/IAMExample.java similarity index 100% rename from feign-core/src/test/java/feign/examples/IAMExample.java rename to core/src/test/java/feign/examples/IAMExample.java diff --git a/examples/feign-example-github/build.gradle b/example-github/build.gradle similarity index 100% rename from examples/feign-example-github/build.gradle rename to example-github/build.gradle diff --git a/examples/feign-example-github/src/main/java/feign/example/github/GitHubExample.java b/example-github/src/main/java/feign/example/github/GitHubExample.java similarity index 100% rename from examples/feign-example-github/src/main/java/feign/example/github/GitHubExample.java rename to example-github/src/main/java/feign/example/github/GitHubExample.java diff --git a/examples/feign-example-wikipedia/build.gradle b/example-wikipedia/build.gradle similarity index 100% rename from examples/feign-example-wikipedia/build.gradle rename to example-wikipedia/build.gradle diff --git a/examples/feign-example-wikipedia/src/main/java/feign/example/wikipedia/ResponseDecoder.java b/example-wikipedia/src/main/java/feign/example/wikipedia/ResponseDecoder.java similarity index 100% rename from examples/feign-example-wikipedia/src/main/java/feign/example/wikipedia/ResponseDecoder.java rename to example-wikipedia/src/main/java/feign/example/wikipedia/ResponseDecoder.java diff --git a/examples/feign-example-wikipedia/src/main/java/feign/example/wikipedia/WikipediaExample.java b/example-wikipedia/src/main/java/feign/example/wikipedia/WikipediaExample.java similarity index 100% rename from examples/feign-example-wikipedia/src/main/java/feign/example/wikipedia/WikipediaExample.java rename to example-wikipedia/src/main/java/feign/example/wikipedia/WikipediaExample.java diff --git a/feign-jaxrs/src/test/java/feign/jaxrs/examples/IAMExample.java b/feign-jaxrs/src/test/java/feign/jaxrs/examples/IAMExample.java deleted file mode 100644 index f303775068..0000000000 --- a/feign-jaxrs/src/test/java/feign/jaxrs/examples/IAMExample.java +++ /dev/null @@ -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 { - - @Override public Class 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("([\\S&&[^<]]+)"); - } - } -} diff --git a/feign-gson/README.md b/gson/README.md similarity index 100% rename from feign-gson/README.md rename to gson/README.md diff --git a/feign-gson/src/main/java/feign/gson/GsonModule.java b/gson/src/main/java/feign/gson/GsonModule.java similarity index 100% rename from feign-gson/src/main/java/feign/gson/GsonModule.java rename to gson/src/main/java/feign/gson/GsonModule.java diff --git a/feign-gson/src/test/java/feign/gson/GsonModuleTest.java b/gson/src/test/java/feign/gson/GsonModuleTest.java similarity index 100% rename from feign-gson/src/test/java/feign/gson/GsonModuleTest.java rename to gson/src/test/java/feign/gson/GsonModuleTest.java diff --git a/feign-jaxrs/README.md b/jaxrs/README.md similarity index 100% rename from feign-jaxrs/README.md rename to jaxrs/README.md diff --git a/feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java b/jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java similarity index 100% rename from feign-jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java rename to jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java diff --git a/feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java b/jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java similarity index 100% rename from feign-jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java rename to jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java diff --git a/feign-jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java b/jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java similarity index 100% rename from feign-jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java rename to jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java diff --git a/feign-ribbon/README.md b/ribbon/README.md similarity index 100% rename from feign-ribbon/README.md rename to ribbon/README.md diff --git a/feign-ribbon/src/main/java/feign/ribbon/LBClient.java b/ribbon/src/main/java/feign/ribbon/LBClient.java similarity index 100% rename from feign-ribbon/src/main/java/feign/ribbon/LBClient.java rename to ribbon/src/main/java/feign/ribbon/LBClient.java diff --git a/feign-ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java b/ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java similarity index 100% rename from feign-ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java rename to ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java diff --git a/feign-ribbon/src/main/java/feign/ribbon/RibbonModule.java b/ribbon/src/main/java/feign/ribbon/RibbonModule.java similarity index 100% rename from feign-ribbon/src/main/java/feign/ribbon/RibbonModule.java rename to ribbon/src/main/java/feign/ribbon/RibbonModule.java diff --git a/feign-ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java b/ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java similarity index 100% rename from feign-ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java rename to ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java diff --git a/feign-ribbon/src/test/java/feign/ribbon/RibbonClientTest.java b/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java similarity index 100% rename from feign-ribbon/src/test/java/feign/ribbon/RibbonClientTest.java rename to ribbon/src/test/java/feign/ribbon/RibbonClientTest.java diff --git a/settings.gradle b/settings.gradle index bd5c8dd9ea..a7bf699763 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,6 @@ rootProject.name='feign' -include 'feign-core', 'feign-gson', 'feign-jaxrs', 'feign-ribbon', 'examples:feign-example-github', 'examples:feign-example-wikipedia' +include 'core', 'gson', 'jaxrs', 'ribbon', 'example-github', 'example-wikipedia' + +rootProject.children.each { childProject -> + childProject.name = 'feign-' + childProject.name +} From 29839c97b7a31af2a97dcea4569c2167de962cdd Mon Sep 17 00:00:00 2001 From: adriancole Date: Sat, 17 Aug 2013 15:20:28 -0700 Subject: [PATCH 087/179] added dagger IDE setup for annotation parsing via gradle idea and eclipse plugins --- .gitignore | 2 + build.gradle | 9 +-- dagger.gradle | 178 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 182 insertions(+), 7 deletions(-) create mode 100644 dagger.gradle diff --git a/.gitignore b/.gitignore index 5b07c032e3..7adeb75184 100644 --- a/.gitignore +++ b/.gitignore @@ -63,6 +63,8 @@ atlassian-ide-plugin.xml .project .settings .metadata +.factorypath +.generated # NetBeans specific files/directories .nbattrs diff --git a/build.gradle b/build.gradle index df7ad91b4e..7168bffff1 100644 --- a/build.gradle +++ b/build.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,8 +37,6 @@ project(':feign-core') { } dependencies { - 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' @@ -55,7 +55,6 @@ project(':feign-gson') { dependencies { compile project(':feign-core') compile 'com.google.code.gson:gson:2.2.4' - provided 'com.squareup.dagger:dagger-compiler:1.1.0' testCompile 'org.testng:testng:6.8.5' } } @@ -70,9 +69,6 @@ project(':feign-jaxrs') { dependencies { compile project(':feign-core') compile 'javax.ws.rs:jsr311-api:1.1.1' - provided 'com.squareup.dagger:dagger-compiler:1.1.0' - // 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.5' @@ -89,7 +85,6 @@ project(':feign-ribbon') { dependencies { compile project(':feign-core') compile 'com.netflix.ribbon:ribbon-core:0.2.0' - provided 'com.squareup.dagger:dagger-compiler:1.1.0' testCompile 'org.testng:testng:6.8.5' testCompile 'com.google.mockwebserver:mockwebserver:20130706' } diff --git a/dagger.gradle b/dagger.gradle new file mode 100644 index 0000000000..599960266f --- /dev/null +++ b/dagger.gradle @@ -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 + } + } + } + } +} + From 8c06dd04d5463c1e10ef17cf44d68beb040ecd0d Mon Sep 17 00:00:00 2001 From: adriancole Date: Sun, 18 Aug 2013 10:13:04 -0700 Subject: [PATCH 088/179] closes #35 add RequestInterceptor --- CHANGES.md | 3 + README.md | 19 +++++ core/src/main/java/feign/MethodHandler.java | 35 +++++++--- core/src/main/java/feign/ReflectiveFeign.java | 11 ++- .../main/java/feign/RequestInterceptor.java | 70 +++++++++++++++++++ core/src/main/java/feign/RequestTemplate.java | 15 +--- core/src/main/java/feign/Target.java | 2 +- core/src/test/java/feign/FeignTest.java | 59 ++++++++++++++++ 8 files changed, 183 insertions(+), 31 deletions(-) create mode 100644 core/src/main/java/feign/RequestInterceptor.java diff --git a/CHANGES.md b/CHANGES.md index 6f156f77fa..176ef6a034 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,6 @@ +### Version 4.3 +* Add ability to configure zero or more RequestInterceptors. + ### 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 diff --git a/README.md b/README.md index c3349f60ce..e04dedc0a5 100644 --- a/README.md +++ b/README.md @@ -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` will invoke a new http request for each call to `subscribe()`. This is the async equivalent to an `Iterable`. Here's how one looks: diff --git a/core/src/main/java/feign/MethodHandler.java b/core/src/main/java/feign/MethodHandler.java index d7cbffff12..173285da5e 100644 --- a/core/src/main/java/feign/MethodHandler.java +++ b/core/src/main/java/feign/MethodHandler.java @@ -27,6 +27,7 @@ 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 @@ static class Factory { private final Client client; private final Lazy httpExecutor; private final Provider retryer; + private final Set requestInterceptors; private final Logger logger; private final Provider logLevel; - @Inject Factory(Client client, @Named("http") Lazy httpExecutor, Provider retryer, Logger logger, - Provider logLevel) { + @Inject Factory(Client client, @Named("http") Lazy httpExecutor, Provider retryer, + Set requestInterceptors, Logger logger, Provider 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 @@ static class ObserverHandler extends BaseMethodHandler { private final Lazy httpExecutor; private final IncrementalDecoder.TextStream incrementalDecoder; - private ObserverHandler(Target target, Client client, Provider retryer, Logger logger, + private ObserverHandler(Target target, Client client, Provider retryer, + Set requestInterceptors, Logger logger, Provider logLevel, MethodMetadata metadata, BuildTemplateFromArgs buildTemplateFromArgs, Options options, IncrementalDecoder.TextStream incrementalDecoder, ErrorDecoder errorDecoder, Lazy 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 @@ private ObserverHandler(Target target, Client client, Provider retry static class SynchronousMethodHandler extends BaseMethodHandler { private final Decoder.TextStream decoder; - private SynchronousMethodHandler(Target target, Client client, Provider retryer, Logger logger, + private SynchronousMethodHandler(Target target, Client client, Provider retryer, + Set requestInterceptors, Logger logger, Provider 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 @@ static abstract class BaseMethodHandler implements MethodHandler { protected final Target target; protected final Client client; protected final Provider retryer; + protected final Set requestInterceptors; protected final Logger logger; protected final Provider logLevel; protected final BuildTemplateFromArgs buildTemplateFromArgs; protected final Options options; protected final ErrorDecoder errorDecoder; - private BaseMethodHandler(Target target, Client client, Provider retryer, Logger logger, + private BaseMethodHandler(Target target, Client client, Provider retryer, + Set requestInterceptors, Logger logger, Provider 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 @@ protected long elapsedTime(long start) { } protected Request targetRequest(RequestTemplate template) { + for (RequestInterceptor interceptor : requestInterceptors) { + interceptor.apply(template); + } return target.apply(new RequestTemplate(template)); } diff --git a/core/src/main/java/feign/ReflectiveFeign.java b/core/src/main/java/feign/ReflectiveFeign.java index 37eebc7dc7..844f7012e1 100644 --- a/core/src/main/java/feign/ReflectiveFeign.java +++ b/core/src/main/java/feign/ReflectiveFeign.java @@ -35,6 +35,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -105,19 +106,17 @@ static class FeignInvocationHandler implements InvocationHandler { } } - @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 noRequestInterceptors() { + return new LinkedHashSet(); + } @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; diff --git a/core/src/main/java/feign/RequestInterceptor.java b/core/src/main/java/feign/RequestInterceptor.java new file mode 100644 index 0000000000..39b79c60b0 --- /dev/null +++ b/core/src/main/java/feign/RequestInterceptor.java @@ -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)}. + *
+ *
+ * For example: + *
+ *
+ * public void apply(RequestTemplate input) {
+ *     input.replaceHeader("X-Auth", currentToken);
+ * }
+ * 
+ *
+ *
Configuration
+ *
+ * {@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. + *
+ *
+ * For example: + *
+ *
+ * {@literal @}Provides(Type = SET) RequestInterceptor addTimestamp(TimestampInterceptor in) {
+ * return in;
+ * }
+ * 
+ *
+ *
Implementation notes
+ *
+ * Do not add parameters, such as {@code /path/{foo}/bar } + * in your implementation of {@link #apply(RequestTemplate)}. + *
+ * Interceptors are applied after the template's parameters are + * {@link RequestTemplate#resolve(java.util.Map) resolved}. This is to ensure + * that you can implement signatures are interceptors. + *
+ *

Relationship to Retrofit 1.x
+ *
+ * This class is similar to {@code RequestInterceptor.intercept()}, + * except that the implementation can read, remove, or otherwise mutate any + * part of the request template. + */ +public interface RequestInterceptor { + /** + * Called for every request. Add data using methods on the supplied {@link RequestTemplate}. + */ + void apply(RequestTemplate template); +} diff --git a/core/src/main/java/feign/RequestTemplate.java b/core/src/main/java/feign/RequestTemplate.java index f3081a1927..6fd35bb003 100644 --- a/core/src/main/java/feign/RequestTemplate.java +++ b/core/src/main/java/feign/RequestTemplate.java @@ -73,19 +73,8 @@ public RequestTemplate(RequestTemplate toCopy) { } /** - * Targets a template to this target, adding the {@link #url() base url} and - * any authentication headers. - *
- *
- * For example: - *
- *
-   * public Request apply(RequestTemplate input) {
-   *     input.insert(0, url());
-   *     input.replaceHeader("X-Auth", currentToken);
-   *     return input.asRequest();
-   * }
-   * 
+ * Resolves any templated variables in the requests path, query, or headers + * against the supplied unencoded arguments. *
*

relationship to JAXRS 2.0
*
diff --git a/core/src/main/java/feign/Target.java b/core/src/main/java/feign/Target.java index d489a10cfe..ab3588cce4 100644 --- a/core/src/main/java/feign/Target.java +++ b/core/src/main/java/feign/Target.java @@ -40,7 +40,7 @@ public interface Target { /** * Targets a template to this target, adding the {@link #url() base url} and - * any authentication headers. + * any target-specific headers or query parameters. *
*
* For example: diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index fba37b919f..550cfd233b 100644 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -18,6 +18,7 @@ import com.google.common.base.Joiner; 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; @@ -308,6 +309,64 @@ public void postBodyParam() throws IOException, InterruptedException { } } + @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().setResponseCode(200).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().setResponseCode(200).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(); + } + } + @Test public void toKeyMethodFormatsAsExpected() throws Exception { assertEquals(Feign.configKey(TestInterface.class.getDeclaredMethod("post")), "TestInterface#post()"); assertEquals(Feign.configKey(TestInterface.class.getDeclaredMethod("uriParam", String.class, URI.class, From fbbd75e34d97219b3b44ce903a6ec40260e31255 Mon Sep 17 00:00:00 2001 From: adriancole Date: Sun, 18 Aug 2013 10:56:43 -0700 Subject: [PATCH 089/179] Remove overrides = true on codec modules --- CHANGES.md | 1 + README.md | 4 +-- core/src/main/java/feign/Feign.java | 12 -------- core/src/main/java/feign/ReflectiveFeign.java | 16 ++++++++-- core/src/test/java/feign/FeignTest.java | 30 +++++++++++-------- .../java/feign/examples/GitHubExample.java | 2 +- .../test/java/feign/examples/IAMExample.java | 2 +- gson/src/main/java/feign/gson/GsonModule.java | 2 +- 8 files changed, 38 insertions(+), 31 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 176ef6a034..d5aa4c3921 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ ### 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 diff --git a/README.md b/README.md index e04dedc0a5..57db11da20 100644 --- a/README.md +++ b/README.md @@ -137,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() { @@ -215,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("([\\S&&[^<]]+)"); diff --git a/core/src/main/java/feign/Feign.java b/core/src/main/java/feign/Feign.java index d5d103ea55..6cc200b1c6 100644 --- a/core/src/main/java/feign/Feign.java +++ b/core/src/main/java/feign/Feign.java @@ -124,18 +124,6 @@ public static class Defaults { return new Options(); } - @Provides Set noEncoders() { - return Collections.emptySet(); - } - - @Provides Set noDecoders() { - return Collections.emptySet(); - } - - @Provides Set noIncrementalDecoders() { - return Collections.emptySet(); - } - /** * Used for both http invocation and decoding when observers are used. */ diff --git a/core/src/main/java/feign/ReflectiveFeign.java b/core/src/main/java/feign/ReflectiveFeign.java index 844f7012e1..2bdb9a114a 100644 --- a/core/src/main/java/feign/ReflectiveFeign.java +++ b/core/src/main/java/feign/ReflectiveFeign.java @@ -33,9 +33,9 @@ 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.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -109,7 +109,19 @@ static class FeignInvocationHandler implements InvocationHandler { @dagger.Module(complete = false, injects = {Feign.class, MethodHandler.Factory.class}, library = true) public static class Module { @Provides(type = Provides.Type.SET_VALUES) Set noRequestInterceptors() { - return new LinkedHashSet(); + return Collections.emptySet(); + } + + @Provides(type = Provides.Type.SET_VALUES) Set noEncoders() { + return Collections.emptySet(); + } + + @Provides(type = Provides.Type.SET_VALUES) Set noDecoders() { + return Collections.emptySet(); + } + + @Provides(type = Provides.Type.SET_VALUES) Set noIncrementalDecoders() { + return Collections.emptySet(); } @Provides Feign provideFeign(ReflectiveFeign in) { diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index 550cfd233b..5034c25624 100644 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -90,7 +90,7 @@ void login( @RequestLine("POST /") Observable observableResponse(); - @dagger.Module(overrides = true, library = true) + @dagger.Module(library = true) static class Module { @Provides(type = SET) Encoder defaultEncoder() { return new Encoder.Text() { @@ -108,14 +108,6 @@ static class Module { }; } - // just run synchronously - @Provides @Singleton @Named("http") Executor httpExecutor() { - return new Executor() { - @Override public void execute(Runnable command) { - command.run(); - } - }; - } } } @@ -126,7 +118,8 @@ public void observableVoid() throws IOException, InterruptedException { server.play(); try { - TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module()); + TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), + new TestInterface.Module(), new RunSynchronous()); final AtomicBoolean success = new AtomicBoolean(); @@ -160,7 +153,8 @@ public void observableResponse() throws IOException, InterruptedException { server.play(); try { - TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module()); + TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), + new TestInterface.Module(), new RunSynchronous()); final AtomicBoolean success = new AtomicBoolean(); @@ -187,6 +181,17 @@ public void observableResponse() throws IOException, InterruptedException { } } + @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(); @@ -194,7 +199,8 @@ public void incrementString() throws IOException, InterruptedException { server.play(); try { - TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module()); + TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), + new TestInterface.Module(), new RunSynchronous()); final AtomicBoolean success = new AtomicBoolean(); diff --git a/core/src/test/java/feign/examples/GitHubExample.java b/core/src/test/java/feign/examples/GitHubExample.java index 080fd1893a..428f968570 100644 --- a/core/src/test/java/feign/examples/GitHubExample.java +++ b/core/src/test/java/feign/examples/GitHubExample.java @@ -97,7 +97,7 @@ static class GitHubModule { /** * 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() { diff --git a/core/src/test/java/feign/examples/IAMExample.java b/core/src/test/java/feign/examples/IAMExample.java index 7f384e2870..f16bb3c274 100644 --- a/core/src/test/java/feign/examples/IAMExample.java +++ b/core/src/test/java/feign/examples/IAMExample.java @@ -63,7 +63,7 @@ private IAMTarget(String accessKey, String secretKey) { } } - @Module(overrides = true, library = true) + @Module(library = true) static class IAMModule { @Provides(type = SET) Decoder decoder() { return Decoders.firstGroup("([\\S&&[^<]]+)"); diff --git a/gson/src/main/java/feign/gson/GsonModule.java b/gson/src/main/java/feign/gson/GsonModule.java index 63873e53a7..aab32687d3 100644 --- a/gson/src/main/java/feign/gson/GsonModule.java +++ b/gson/src/main/java/feign/gson/GsonModule.java @@ -43,7 +43,7 @@ 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) { From f86da0c67c82ae297f639530e9427bb5a0d4a12b Mon Sep 17 00:00:00 2001 From: adriancole Date: Sun, 18 Aug 2013 11:55:20 -0700 Subject: [PATCH 090/179] updated examples to 4.3 syntax --- example-github/build.gradle | 4 +-- .../feign/example/github/GitHubExample.java | 6 ++-- example-wikipedia/build.gradle | 4 +-- .../example/wikipedia/WikipediaExample.java | 29 ++++++++++--------- 4 files changed, 23 insertions(+), 20 deletions(-) diff --git a/example-github/build.gradle b/example-github/build.gradle index 3ca897e3bf..126b8632df 100644 --- a/example-github/build.gradle +++ b/example-github/build.gradle @@ -1,8 +1,8 @@ apply plugin: 'java' dependencies { - compile 'com.netflix.feign:feign-core:4.1.0' - compile 'com.netflix.feign:feign-gson:4.1.0' + compile 'com.netflix.feign:feign-core:4.3.0' + compile 'com.netflix.feign:feign-gson:4.3.0' provided 'com.squareup.dagger:dagger-compiler:1.1.0' } diff --git a/example-github/src/main/java/feign/example/github/GitHubExample.java b/example-github/src/main/java/feign/example/github/GitHubExample.java index 81e9b71f35..6f8977913b 100644 --- a/example-github/src/main/java/feign/example/github/GitHubExample.java +++ b/example-github/src/main/java/feign/example/github/GitHubExample.java @@ -47,7 +47,7 @@ static class Contributor { } public static void main(String... args) throws InterruptedException { - GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GitHubModule()); + GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule(), new LogToStderr()); System.out.println("Let's fetch and print a list of the contributors to this library."); List contributors = github.contributors("netflix", "feign"); @@ -95,8 +95,8 @@ public ContributorObserver(CountDownLatch latch) { } } - @Module(overrides = true, library = true, includes = GsonModule.class) - static class GitHubModule { + @Module(overrides = true, library = true) + static class LogToStderr { @Provides Logger.Level loggingLevel() { return Logger.Level.BASIC; diff --git a/example-wikipedia/build.gradle b/example-wikipedia/build.gradle index 6d9a64d0b8..816eda6481 100644 --- a/example-wikipedia/build.gradle +++ b/example-wikipedia/build.gradle @@ -1,8 +1,8 @@ apply plugin: 'java' dependencies { - compile 'com.netflix.feign:feign-core:4.1.0' - compile 'com.netflix.feign:feign-gson:4.1.0' + compile 'com.netflix.feign:feign-core:4.3.0' + compile 'com.netflix.feign:feign-gson:4.3.0' provided 'com.squareup.dagger:dagger-compiler:1.1.0' } diff --git a/example-wikipedia/src/main/java/feign/example/wikipedia/WikipediaExample.java b/example-wikipedia/src/main/java/feign/example/wikipedia/WikipediaExample.java index 83151288fb..90ee691636 100644 --- a/example-wikipedia/src/main/java/feign/example/wikipedia/WikipediaExample.java +++ b/example-wikipedia/src/main/java/feign/example/wikipedia/WikipediaExample.java @@ -30,8 +30,6 @@ import java.util.Iterator; import static dagger.Provides.Type.SET; -import static feign.Logger.ErrorLogger; -import static feign.Logger.Level.BASIC; public class WikipediaExample { @@ -56,7 +54,8 @@ public static class Response extends ArrayList { } public static void main(String... args) throws InterruptedException { - Wikipedia wikipedia = Feign.create(Wikipedia.class, "http://en.wikipedia.org", new WikipediaModule()); + Wikipedia wikipedia = Feign.create(Wikipedia.class, "http://en.wikipedia.org", + new WikipediaDecoder(), new LogToStderr()); System.out.println("Let's search for PTAL!"); Iterator pages = lazySearch(wikipedia, "PTAL"); @@ -102,16 +101,8 @@ public void remove() { }; } - @Module(overrides = true, library = true, includes = GsonModule.class) - static class WikipediaModule { - - @Provides Logger.Level loggingLevel() { - return BASIC; - } - - @Provides Logger logger() { - return new ErrorLogger(); - } + @Module(library = true, includes = GsonModule.class) + static class WikipediaDecoder { /** * add to the set of Decoders one that handles {@code Response}. @@ -142,4 +133,16 @@ protected Page build(JsonReader reader) throws IOException { }; } } + + @Module(overrides = true, library = true) + static class LogToStderr { + + @Provides Logger.Level loggingLevel() { + return Logger.Level.BASIC; + } + + @Provides Logger logger() { + return new Logger.ErrorLogger(); + } + } } From a91d4b90a0b6b280fe71542956e9383aeeed4035 Mon Sep 17 00:00:00 2001 From: "David M. Carr" Date: Wed, 28 Aug 2013 17:02:37 -0400 Subject: [PATCH 091/179] default client: use custom HostnameVerifier if overridden Sometimes, it's useful to override the hostname verifier for SSL connections. One example, would be when you're developing against a test server managed by another company that's using a self-signed certificate with a mis-matched hostname. This patch enables that usage by overriding the default HostnameVerifier in a Dagger module. Adding test coverage required switching the TrustingSSLSocketFactory from using an anonymous cipher suite to one that authenticates. A test keystore is used for this purpose. It contains two self-signed certificates, one each with alias (and CN) "localhost" and "bad.example.com". The TrustingSSLSocketFactory is no longer a singleton; it now optionally takes a key alias as an argument. --- NOTICE | 4 + core/src/main/java/feign/Client.java | 6 +- core/src/main/java/feign/Feign.java | 7 ++ .../java/feign/AcceptAllHostnameVerifier.java | 26 +++++ core/src/test/java/feign/FeignTest.java | 29 ++++- .../java/feign/TrustingSSLSocketFactory.java | 99 ++++++++++++++++-- core/src/test/resources/keystore.jks | Bin 0 -> 4488 bytes 7 files changed, 157 insertions(+), 14 deletions(-) create mode 100644 NOTICE create mode 100644 core/src/test/java/feign/AcceptAllHostnameVerifier.java create mode 100644 core/src/test/resources/keystore.jks diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000000..53830957de --- /dev/null +++ b/NOTICE @@ -0,0 +1,4 @@ +Feign +Copyright 2013 Netflix, Inc. + +Portions of this software developed by Commerce Technologies, Inc. diff --git a/core/src/main/java/feign/Client.java b/core/src/main/java/feign/Client.java index 315ccd83e0..324e129052 100644 --- a/core/src/main/java/feign/Client.java +++ b/core/src/main/java/feign/Client.java @@ -28,6 +28,7 @@ import java.util.Map; import javax.inject.Inject; +import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLSocketFactory; @@ -55,9 +56,11 @@ public interface Client { public static class Default implements Client { private final Lazy sslContextFactory; + private final Lazy hostnameVerifier; - @Inject public Default(Lazy sslContextFactory) { + @Inject public Default(Lazy sslContextFactory, Lazy hostnameVerifier) { this.sslContextFactory = sslContextFactory; + this.hostnameVerifier = hostnameVerifier; } @Override public Response execute(Request request, Options options) throws IOException { @@ -70,6 +73,7 @@ HttpURLConnection convertAndSend(Request request, Options options) throws IOExce if (connection instanceof HttpsURLConnection) { HttpsURLConnection sslCon = (HttpsURLConnection) connection; sslCon.setSSLSocketFactory(sslContextFactory.get()); + sslCon.setHostnameVerifier(hostnameVerifier.get()); } connection.setConnectTimeout(options.connectTimeoutMillis()); connection.setReadTimeout(options.readTimeoutMillis()); diff --git a/core/src/main/java/feign/Feign.java b/core/src/main/java/feign/Feign.java index 6cc200b1c6..f92841e9b0 100644 --- a/core/src/main/java/feign/Feign.java +++ b/core/src/main/java/feign/Feign.java @@ -29,6 +29,8 @@ 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 static class Defaults { return SSLSocketFactory.class.cast(SSLSocketFactory.getDefault()); } + @Provides + HostnameVerifier hostnameVerifier() { + return HttpsURLConnection.getDefaultHostnameVerifier(); + } + @Provides Client httpClient(Client.Default client) { return client; } diff --git a/core/src/test/java/feign/AcceptAllHostnameVerifier.java b/core/src/test/java/feign/AcceptAllHostnameVerifier.java new file mode 100644 index 0000000000..fa0055dba3 --- /dev/null +++ b/core/src/test/java/feign/AcceptAllHostnameVerifier.java @@ -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; + } +} diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index 5034c25624..0512e3ac17 100644 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -31,6 +31,7 @@ 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; @@ -522,7 +523,7 @@ public void doesntRetryAfterResponseIsSent() throws IOException, InterruptedExce } } - @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(); @@ -531,7 +532,7 @@ static class TrustSSLSockets { @Test public void canOverrideSSLSocketFactory() throws IOException, InterruptedException { MockWebServer server = new MockWebServer(); - server.useHttps(TrustingSSLSocketFactory.get(), false); + server.useHttps(TrustingSSLSocketFactory.get("localhost"), false); server.enqueue(new MockResponse().setResponseCode(200).setBody("success!".getBytes())); server.play(); @@ -544,9 +545,31 @@ static class TrustSSLSockets { } } + @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().setResponseCode(200).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.play(); diff --git a/core/src/test/java/feign/TrustingSSLSocketFactory.java b/core/src/test/java/feign/TrustingSSLSocketFactory.java index fc08cc13bc..15d3eae6e2 100644 --- a/core/src/test/java/feign/TrustingSSLSocketFactory.java +++ b/core/src/test/java/feign/TrustingSSLSocketFactory.java @@ -15,11 +15,24 @@ */ 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; @@ -27,22 +40,40 @@ 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 so that they can avoid having to read a keystore. + * Used for ssl tests to simplify setup. */ -final class TrustingSSLSocketFactory extends SSLSocketFactory implements X509TrustManager, KeyManager { +final class TrustingSSLSocketFactory extends SSLSocketFactory implements X509TrustManager, X509KeyManager { + + private static LoadingCache sslSocketFactories = + CacheBuilder.newBuilder().build(new CacheLoader() { + @Override + public SSLSocketFactory load(String serverAlias) throws Exception { + return new TrustingSSLSocketFactory(serverAlias); + } + }); public static SSLSocketFactory get() { - return Singleton.INSTANCE.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() { + private TrustingSSLSocketFactory(String serverAlias) { try { SSLContext sc = SSLContext.getInstance("SSL"); sc.init(new KeyManager[]{this}, new TrustManager[]{this}, new SecureRandom()); @@ -50,6 +81,20 @@ private TrustingSSLSocketFactory() { } 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() { @@ -100,15 +145,49 @@ 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"}; + @Override + public String[] getClientAliases(String keyType, Principal[] issuers) { + return null; + } - private static enum Singleton implements Provider { - INSTANCE; + @Override + public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) { + return null; + } - private final SSLSocketFactory sslSocketFactory = new TrustingSSLSocketFactory(); + @Override + public String[] getServerAliases(String keyType, Principal[] issuers) { + return null; + } - @Override public SSLSocketFactory get() { - return sslSocketFactory; + @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 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"}; } diff --git a/core/src/test/resources/keystore.jks b/core/src/test/resources/keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..19108e3579c01fead1beb39b69ccfbd066c0edff GIT binary patch literal 4488 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 literal 0 HcmV?d00001 From 94a087a41c7f364c9d0c3cceb5c7855c8ea84b98 Mon Sep 17 00:00:00 2001 From: "David M. Carr" Date: Wed, 28 Aug 2013 19:39:51 -0400 Subject: [PATCH 092/179] default client: add support for gzip-encoded request bodies (#52) Enhances the default client to GZIP-encode request bodies when the appropriate content-encoding header is set in the interface's method definition. https://github.com/Netflix/feign/issues/52 --- CHANGES.md | 4 +++ core/src/main/java/feign/Client.java | 17 ++++++++-- core/src/main/java/feign/Util.java | 8 +++++ core/src/test/java/feign/FeignTest.java | 31 ++++++++++++++++- core/src/test/java/feign/GZIPStreams.java | 41 +++++++++++++++++++++++ 5 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 core/src/test/java/feign/GZIPStreams.java diff --git a/CHANGES.md b/CHANGES.md index d5aa4c3921..6a030b0257 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,7 @@ +### Version 4.4 +* Support overriding default HostnameVerifier +* Support GZIP content encoding for request bodies + ### Version 4.3 * Add ability to configure zero or more RequestInterceptors. * Remove `overrides = true` on codec modules. diff --git a/core/src/main/java/feign/Client.java b/core/src/main/java/feign/Client.java index 324e129052..be6a0ba179 100644 --- a/core/src/main/java/feign/Client.java +++ b/core/src/main/java/feign/Client.java @@ -26,6 +26,7 @@ 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; @@ -35,7 +36,9 @@ 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; /** @@ -81,13 +84,20 @@ HttpURLConnection convertAndSend(Request request, Options options) throws IOExce connection.setInstanceFollowRedirects(true); connection.setRequestMethod(request.method()); + Collection 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); } } @@ -99,6 +109,9 @@ HttpURLConnection convertAndSend(Request request, Options options) throws IOExce } connection.setDoOutput(true); OutputStream out = connection.getOutputStream(); + if (gzipEncodedRequest) { + out = new GZIPOutputStream(out); + } try { out.write(request.body().getBytes(UTF_8)); } finally { diff --git a/core/src/main/java/feign/Util.java b/core/src/main/java/feign/Util.java index eceb6139a5..c251c31b6a 100644 --- a/core/src/main/java/feign/Util.java +++ b/core/src/main/java/feign/Util.java @@ -39,10 +39,18 @@ private Util() { // no instances * 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 /** diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index 0512e3ac17..224cb65e93 100644 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -16,6 +16,8 @@ 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; @@ -47,7 +49,9 @@ 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.assertNull; import static org.testng.Assert.assertTrue; import static org.testng.Assert.fail; @@ -80,6 +84,8 @@ void login( @RequestLine("POST /") void body(List contents); + @RequestLine("POST /") @Headers("Content-Encoding: gzip") void gzipBody(List contents); + @RequestLine("POST /") void form( @Named("customer_name") String customer, @Named("user_name") String user, @Named("password") String password); @@ -310,7 +316,30 @@ public void postBodyParam() throws IOException, InterruptedException { 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().setResponseCode(200).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(); } diff --git a/core/src/test/java/feign/GZIPStreams.java b/core/src/test/java/feign/GZIPStreams.java new file mode 100644 index 0000000000..42b2886825 --- /dev/null +++ b/core/src/test/java/feign/GZIPStreams.java @@ -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 newInputStreamSupplier(InputSupplier supplier) { + return new GZIPInputStreamSupplier(supplier); + } + + private static class GZIPInputStreamSupplier implements InputSupplier { + private final InputSupplier supplier; + + GZIPInputStreamSupplier(InputSupplier supplier) { + this.supplier = supplier; + } + + @Override + public GZIPInputStream getInput() throws IOException { + return new GZIPInputStream(supplier.getInput()); + } + } +} From fa3aee6ed7640ebcd2ea29669c505c5e10f6e920 Mon Sep 17 00:00:00 2001 From: adriancole Date: Thu, 29 Aug 2013 17:10:03 -0700 Subject: [PATCH 093/179] issue #55: support iterable query params --- CHANGES.md | 1 + core/src/main/java/feign/RequestTemplate.java | 13 ++++- core/src/test/java/feign/FeignTest.java | 55 ++++++++++++------- .../test/java/feign/RequestTemplateTest.java | 16 ++++++ 4 files changed, 63 insertions(+), 22 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 6a030b0257..048da2402f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,7 @@ ### Version 4.4 * Support overriding default HostnameVerifier * Support GZIP content encoding for request bodies +* Support Iterable args for query parameters ### Version 4.3 * Add ability to configure zero or more RequestInterceptors. diff --git a/core/src/main/java/feign/RequestTemplate.java b/core/src/main/java/feign/RequestTemplate.java index 6fd35bb003..b6df8661d6 100644 --- a/core/src/main/java/feign/RequestTemplate.java +++ b/core/src/main/java/feign/RequestTemplate.java @@ -84,11 +84,11 @@ public RequestTemplate(RequestTemplate toCopy) { * just the URL */ public RequestTemplate resolve(Map unencoded) { + replaceQueryValues(unencoded); Map encoded = new LinkedHashMap(); for (Entry entry : unencoded.entrySet()) { encoded.put(entry.getKey(), urlEncode(String.valueOf(entry.getValue()))); } - replaceQueryValues(encoded); String resolvedUrl = expand(url.toString(), encoded).replace("%2F", "/"); url = new StringBuilder(resolvedUrl); @@ -509,8 +509,15 @@ public void replaceQueryValues(Map unencoded) { 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)); + 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); diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index 224cb65e93..c0dc93e378 100644 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -91,6 +91,8 @@ void login( @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 twos); + @RequestLine("POST /") Observable observableVoid(); @RequestLine("POST /") Observable observableString(); @@ -114,14 +116,29 @@ static class Module { } }; } + } + } + + @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(); } } @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 { @@ -156,7 +173,7 @@ public void observableVoid() throws IOException, InterruptedException { @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 { @@ -202,7 +219,7 @@ static class RunSynchronous { @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 { @@ -237,8 +254,8 @@ public void incrementString() throws IOException, InterruptedException { @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 { @@ -275,7 +292,7 @@ public void multipleObservers() throws IOException, InterruptedException { @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 { @@ -292,7 +309,7 @@ public void postTemplateParamsResolve() throws IOException, InterruptedException @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 { @@ -309,7 +326,7 @@ public void postFormParams() throws IOException, InterruptedException { @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 { @@ -327,7 +344,7 @@ public void postBodyParam() throws IOException, InterruptedException { @Test public void postGZIPEncodedBodyParam() 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 { @@ -359,7 +376,7 @@ static class ForwardedForInterceptor implements RequestInterceptor { @Test public void singleInterceptor() 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 { @@ -387,7 +404,7 @@ static class UserAgentInterceptor implements RequestInterceptor { @Test public void multipleInterceptor() 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 { @@ -445,7 +462,7 @@ public void canOverrideErrorDecoder() throws IOException, InterruptedException { @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 { @@ -474,7 +491,7 @@ public String decode(Reader reader, Type type) throws IOException { 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 { @@ -508,8 +525,8 @@ public String decode(Reader reader, Type type) throws RetryableException, IOExce */ 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 { @@ -538,7 +555,7 @@ public String decode(Reader reader, Type type) throws IOException { @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 { @@ -562,7 +579,7 @@ static class TrustSSLSockets { @Test public void canOverrideSSLSocketFactory() throws IOException, InterruptedException { MockWebServer server = new MockWebServer(); server.useHttps(TrustingSSLSocketFactory.get("localhost"), false); - server.enqueue(new MockResponse().setResponseCode(200).setBody("success!".getBytes())); + server.enqueue(new MockResponse().setBody("success!".getBytes())); server.play(); try { @@ -584,7 +601,7 @@ static class DisableHostnameVerification { @Test public void canOverrideHostnameVerifier() throws IOException, InterruptedException { MockWebServer server = new MockWebServer(); server.useHttps(TrustingSSLSocketFactory.get("bad.example.com"), false); - server.enqueue(new MockResponse().setResponseCode(200).setBody("success!".getBytes())); + server.enqueue(new MockResponse().setBody("success!".getBytes())); server.play(); try { @@ -600,7 +617,7 @@ static class DisableHostnameVerification { MockWebServer server = new MockWebServer(); 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 { diff --git a/core/src/test/java/feign/RequestTemplateTest.java b/core/src/test/java/feign/RequestTemplateTest.java index ffc13e9deb..2e5cd09da9 100644 --- a/core/src/test/java/feign/RequestTemplateTest.java +++ b/core/src/test/java/feign/RequestTemplateTest.java @@ -20,6 +20,8 @@ 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; @@ -83,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. 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")// From 0fbeb1241ad4fa2504316a7564d223ad7578320c Mon Sep 17 00:00:00 2001 From: adriancole Date: Fri, 30 Aug 2013 12:10:04 -0700 Subject: [PATCH 094/179] Support urls which have query parameters --- CHANGES.md | 7 +++--- core/src/main/java/feign/RequestTemplate.java | 3 +-- .../test/java/feign/RequestTemplateTest.java | 22 +++++++++++++++++++ 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 048da2402f..f776d34369 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,8 @@ ### Version 4.4 -* Support overriding default HostnameVerifier -* Support GZIP content encoding for request bodies -* Support Iterable args for query parameters +* 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. diff --git a/core/src/main/java/feign/RequestTemplate.java b/core/src/main/java/feign/RequestTemplate.java index b6df8661d6..fc3f7bd138 100644 --- a/core/src/main/java/feign/RequestTemplate.java +++ b/core/src/main/java/feign/RequestTemplate.java @@ -203,8 +203,7 @@ public RequestTemplate append(CharSequence value) { /* @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; } diff --git a/core/src/test/java/feign/RequestTemplateTest.java b/core/src/test/java/feign/RequestTemplateTest.java index 2e5cd09da9..bc1f31a8d2 100644 --- a/core/src/test/java/feign/RequestTemplateTest.java +++ b/core/src/test/java/feign/RequestTemplateTest.java @@ -122,6 +122,28 @@ ImmutableListMultimap. builder() + "/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.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}\", " + From a7128264c81348bff0075bbf4bb07c509dca24ad Mon Sep 17 00:00:00 2001 From: "David M. Carr" Date: Fri, 30 Aug 2013 15:55:06 -0400 Subject: [PATCH 095/179] Fix NullPointerException when equals or hashCode are called on proxy instance They look something like this: java.lang.NullPointerException at feign.ReflectiveFeign$FeignInvocationHandler.invoke(ReflectiveFeign.java:88) at feign.$Proxy16.equals(Unknown Source) In my particular instance, I had a proxy created by Feign registered in a Spring application context, and it resulted in a NullPointerException on application shutdown. --- CHANGES.md | 3 +++ core/src/main/java/feign/ReflectiveFeign.java | 20 +++++++++++++++++-- core/src/test/java/feign/FeignTest.java | 20 +++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index f776d34369..c8cb710674 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,6 @@ +### 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. diff --git a/core/src/main/java/feign/ReflectiveFeign.java b/core/src/main/java/feign/ReflectiveFeign.java index 2bdb9a114a..0cb2490caa 100644 --- a/core/src/main/java/feign/ReflectiveFeign.java +++ b/core/src/main/java/feign/ReflectiveFeign.java @@ -85,6 +85,17 @@ static class FeignInvocationHandler implements InvocationHandler { } @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); } @@ -93,10 +104,15 @@ static class FeignInvocationHandler implements InvocationHandler { } @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); } diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index c0dc93e378..e604d5dc01 100644 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -51,6 +51,7 @@ 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; @@ -135,6 +136,10 @@ public void iterableQueryParams() throws IOException, InterruptedException { } } + interface OtherTestInterface { + @RequestLine("POST /") String post(); + } + @Test public void observableVoid() throws IOException, InterruptedException { final MockWebServer server = new MockWebServer(); @@ -629,4 +634,19 @@ static class DisableHostnameVerification { 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()); + } } From 1af6fb364f76db4a4013295deefbfa84fe726dee Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Wed, 11 Sep 2013 17:48:04 +0200 Subject: [PATCH 096/179] issue #53: update readme with a warning --- README.md | 60 ++++--------------------------------------------------- 1 file changed, 4 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 57db11da20..822f0d9ec4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # Feign makes writing java http clients easier -Feign is a java to http client binder inspired by [Dagger](https://github.com/square/dagger), [Retrofit](https://github.com/square/retrofit), [RxJava](https://github.com/Netflix/RxJava), [JAXRS-2.0](https://jax-rs-spec.java.net/nonav/2.0/apidocs/index.html), and [WebSocket](http://www.oracle.com/technetwork/articles/java/jsr356-1937161.html). Feign's first goal was reducing the complexity of binding [Denominator](https://github.com/Netflix/Denominator) uniformly to http apis regardless of [restfulness](http://www.slideshare.net/adrianfcole/99problems). +Feign is a java to http client binder inspired by [Dagger](https://github.com/square/dagger), [Retrofit](https://github.com/square/retrofit), [JAXRS-2.0](https://jax-rs-spec.java.net/nonav/2.0/apidocs/index.html), and [WebSocket](http://www.oracle.com/technetwork/articles/java/jsr356-1937161.html). Feign's first goal was reducing the complexity of binding [Denominator](https://github.com/Netflix/Denominator) uniformly to http apis regardless of [restfulness](http://www.slideshare.net/adrianfcole/99problems). + +## Disclaimer +Feign is experimental and [being simplified further](https://github.com/Netflix/feign/issues/53) in version 5. Particularly, this will impact how encoders and encoders are declared, and remove support for observable methods. ### Why Feign and not X? @@ -56,39 +59,6 @@ static class ForwardedForInterceptor implements RequestInterceptor { 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` will invoke a new http request for each call to `subscribe()`. This is the async equivalent to an `Iterable`. -Here's how one looks: -```java -Observable observable = github.contributorsObservable("netflix", "feign"); -subscription = observable.subscribe(newObserver()); -subscription = observable.subscribe(newObserver()); -``` - -`Observer` is fired as a background which adds new elements as they are decoded, or until `subscription.unsubscribe()` is called. Think of `Observer` as an asynchronous equivalent to a lazy sequence. - -Here's how one looks: -```java -Observer printlnObserver = new Observer() { - - public int count; - - @Override public void onNext(Contributor element) { - count++; - } - - @Override public void onSuccess() { - System.out.println("found " + count + " contributors"); - } - - @Override public void onFailure(Throwable cause) { - cause.printStackTrace(); - } -}; -``` - -For more robust integration with `Observable` check out [RxJava](https://github.com/Netflix/RxJava). - ### Multiple Interfaces Feign can produce multiple api interfaces. These are defined as `Target` (default `HardCodedTarget`), which allow for dynamic discovery and decoration of requests prior to execution. @@ -158,29 +128,7 @@ The generic parameter of `Decoder.TextStream` designates which The type param return new SAXDecoder(handlers){}; } ``` -#### Incremental Decoding -The last argument to `Feign.create` allows you to specify additional configuration such as how to decode a responses, modeled in Dagger. - -When using an `IncrementalCallback`, if `T` is not `Void` or `String`, you'll need to configure an `IncrementalDecoder.TextStream` or a general one for all types (`IncrementalDecoder.TextStream`). -The `GsonModule` in the `feign-gson` extension configures a (`IncrementalDecoder.TextStream`) which parses objects from json using reflection. - -Here's how you could write this yourself, using whatever library you prefer: -```java -@Provides(type = SET) IncrementalDecoder incrementalDecoder(final JsonParser parser) { - return new IncrementalDecoder.TextStream() { - - @Override - public void decode(Reader reader, Type type, IncrementalCallback observer) throws IOException { - jsonReader.beginArray(); - while (jsonReader.hasNext()) { - observer.onNext(parser.readJson(reader, type)); - } - jsonReader.endArray(); - } - }; -} -``` ### Advanced usage and Dagger #### Dagger Feign can be directly wired into Dagger which keeps things at compile time and Android friendly. As opposed to exposing builders for config, Feign intends users to embed their config in Dagger. From 449aea577c0c746a87950b0d806e6f861a1cd063 Mon Sep 17 00:00:00 2001 From: adriancole Date: Wed, 11 Sep 2013 19:38:50 +0200 Subject: [PATCH 097/179] Remove support for Observable methods. --- CHANGES.md | 3 + core/src/main/java/feign/Contract.java | 12 -- core/src/main/java/feign/Feign.java | 51 +---- core/src/main/java/feign/MethodHandler.java | 197 +++--------------- core/src/main/java/feign/MethodMetadata.java | 15 -- core/src/main/java/feign/Observable.java | 39 ---- core/src/main/java/feign/Observer.java | 68 ------ core/src/main/java/feign/ReflectiveFeign.java | 55 +---- core/src/main/java/feign/Subscription.java | 32 --- .../java/feign/codec/IncrementalDecoder.java | 117 ----------- .../feign/codec/StringIncrementalDecoder.java | 33 --- .../test/java/feign/DefaultContractTest.java | 37 ---- core/src/test/java/feign/FeignTest.java | 169 --------------- .../java/feign/examples/GitHubExample.java | 62 +----- gson/src/main/java/feign/gson/GsonModule.java | 18 +- .../test/java/feign/gson/GsonModuleTest.java | 46 ---- .../java/feign/jaxrs/JAXRSContractTest.java | 37 ---- .../feign/jaxrs/examples/GitHubExample.java | 46 ---- 18 files changed, 52 insertions(+), 985 deletions(-) delete mode 100644 core/src/main/java/feign/Observable.java delete mode 100644 core/src/main/java/feign/Observer.java delete mode 100644 core/src/main/java/feign/Subscription.java delete mode 100644 core/src/main/java/feign/codec/IncrementalDecoder.java delete mode 100644 core/src/main/java/feign/codec/StringIncrementalDecoder.java diff --git a/CHANGES.md b/CHANGES.md index c8cb710674..3cf46c2c1f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,6 @@ +### Version 5.0 +* Remove support for Observable methods. + ### Version 4.4.1 * Fix NullPointerException on calling equals and hashCode. diff --git a/core/src/main/java/feign/Contract.java b/core/src/main/java/feign/Contract.java index eed9b7bd1a..813247401c 100644 --- a/core/src/main/java/feign/Contract.java +++ b/core/src/main/java/feign/Contract.java @@ -18,7 +18,6 @@ import javax.inject.Named; import java.lang.annotation.Annotation; import java.lang.reflect.Method; -import java.lang.reflect.Type; import java.net.URI; import java.util.ArrayList; import java.util.Collection; @@ -26,7 +25,6 @@ import static feign.Util.checkState; import static feign.Util.emptyToNull; -import static feign.Util.resolveLastTypeParameter; /** * Defines what annotations and values are valid on interfaces. @@ -58,14 +56,6 @@ public MethodMetadata parseAndValidatateMetadata(Method method) { data.returnType(method.getGenericReturnType()); data.configKey(Feign.configKey(method)); - if (Observable.class.isAssignableFrom(method.getReturnType())) { - Type context = method.getGenericReturnType(); - Type observableType = resolveLastTypeParameter(method.getGenericReturnType(), Observable.class); - checkState(observableType != null, "Expected param %s to be Observable or Observable or a subtype", - context, observableType); - data.incrementalType(observableType); - } - for (Annotation methodAnnotation : method.getAnnotations()) { processAnnotationOnMethod(data, methodAnnotation, method); } @@ -83,8 +73,6 @@ public MethodMetadata parseAndValidatateMetadata(Method method) { if (parameterTypes[i] == URI.class) { data.urlIndex(i); } else if (!isHttpAnnotation) { - checkState(!Observer.class.isAssignableFrom(parameterTypes[i]), - "Please return Observer as opposed to passing an Observable arg: %s", method); checkState(data.formParams().isEmpty(), "Body parameters cannot be used with form parameters."); checkState(data.bodyIndex() == null, "Method has too many Body parameters: %s", method); data.bodyIndex(i); diff --git a/core/src/main/java/feign/Feign.java b/core/src/main/java/feign/Feign.java index f92841e9b0..f4e8c1f48d 100644 --- a/core/src/main/java/feign/Feign.java +++ b/core/src/main/java/feign/Feign.java @@ -16,34 +16,19 @@ package feign; -import dagger.Lazy; import dagger.ObjectGraph; import dagger.Provides; import feign.Logger.NoOpLogger; import feign.Request.Options; import feign.Target.HardCodedTarget; -import feign.codec.Decoder; -import feign.codec.Encoder; import feign.codec.ErrorDecoder; -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; import java.util.ArrayList; -import java.util.Collections; import java.util.List; -import java.util.Set; -import java.util.concurrent.Executor; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ThreadFactory; - -import static java.lang.Thread.MIN_PRIORITY; /** * Feign's purpose is to ease development against http apis that feign @@ -52,7 +37,7 @@ * In implementation, Feign is a {@link Feign#newInstance factory} for * generating {@link Target targeted} http apis. */ -public abstract class Feign implements Closeable { +public abstract class Feign { /** * Returns a new instance of an HTTP API, defined by annotations in the @@ -106,9 +91,8 @@ public static class Defaults { return SSLSocketFactory.class.cast(SSLSocketFactory.getDefault()); } - @Provides - HostnameVerifier hostnameVerifier() { - return HttpsURLConnection.getDefaultHostnameVerifier(); + @Provides HostnameVerifier hostnameVerifier() { + return HttpsURLConnection.getDefaultHostnameVerifier(); } @Provides Client httpClient(Client.Default client) { @@ -130,22 +114,6 @@ HostnameVerifier hostnameVerifier() { @Provides Options options() { return new Options(); } - - /** - * Used for both http invocation and decoding when observers are used. - */ - @Provides @Singleton @Named("http") Executor httpExecutor() { - return Executors.newCachedThreadPool(new ThreadFactory() { - @Override public Thread newThread(final Runnable r) { - return new Thread(new Runnable() { - @Override public void run() { - Thread.currentThread().setPriority(MIN_PRIORITY); - r.run(); - } - }, MethodHandler.IDLE_THREAD_NAME); - } - }); - } } /** @@ -188,17 +156,4 @@ private static List modulesForGraph(Object... modules) { modulesForGraph.add(module); return modulesForGraph; } - - private final Lazy httpExecutor; - - Feign(Lazy httpExecutor) { - this.httpExecutor = httpExecutor; - } - - @Override public void close() { - Executor e = httpExecutor.get(); - if (e instanceof ExecutorService) { - ExecutorService.class.cast(e).shutdownNow(); - } - } } diff --git a/core/src/main/java/feign/MethodHandler.java b/core/src/main/java/feign/MethodHandler.java index 173285da5e..767f1e4bfa 100644 --- a/core/src/main/java/feign/MethodHandler.java +++ b/core/src/main/java/feign/MethodHandler.java @@ -15,22 +15,16 @@ */ package feign; -import dagger.Lazy; import feign.Request.Options; import feign.codec.DecodeException; import feign.codec.Decoder; import feign.codec.ErrorDecoder; -import feign.codec.IncrementalDecoder; import javax.inject.Inject; -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; import static feign.FeignException.errorExecuting; import static feign.FeignException.errorReading; @@ -43,16 +37,14 @@ interface MethodHandler { static class Factory { private final Client client; - private final Lazy httpExecutor; private final Provider retryer; private final Set requestInterceptors; private final Logger logger; private final Provider logLevel; - @Inject Factory(Client client, @Named("http") Lazy httpExecutor, Provider retryer, - Set requestInterceptors, Logger logger, Provider logLevel) { + @Inject Factory(Client client, Provider retryer, Set requestInterceptors, + Logger logger, Provider 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"); @@ -64,14 +56,6 @@ public MethodHandler create(Target target, MethodMetadata md, BuildTemplateFr 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, requestInterceptors, logger, - logLevel, md, buildTemplateFromArgs, options, incrementalDecoder, errorDecoder, httpExecutor); - return new ObservableMethodHandler(observerHandler); - } } /** @@ -81,158 +65,31 @@ interface BuildTemplateFromArgs { public RequestTemplate apply(Object[] argv); } - static class ObservableMethodHandler implements MethodHandler { - private final ObserverHandler observerHandler; - - private ObservableMethodHandler(ObserverHandler observerHandler) { - this.observerHandler = observerHandler; - } - - @Override public Object invoke(Object[] argv) { - final Object[] argvCopy = new Object[argv != null ? argv.length : 0]; - if (argv != null) - System.arraycopy(argv, 0, argvCopy, 0, argv.length); - - return new Observable() { - - @Override public Subscription subscribe(Observer observer) { - final Object[] oneMoreArg = new Object[argvCopy.length + 1]; - System.arraycopy(argvCopy, 0, oneMoreArg, 0, argvCopy.length); - oneMoreArg[argvCopy.length] = observer; - return observerHandler.invoke(oneMoreArg); - } - }; - } - } - - static class ObserverHandler extends BaseMethodHandler { - private final Lazy httpExecutor; - private final IncrementalDecoder.TextStream incrementalDecoder; - - private ObserverHandler(Target target, Client client, Provider retryer, - Set requestInterceptors, Logger logger, - Provider logLevel, MethodMetadata metadata, - BuildTemplateFromArgs buildTemplateFromArgs, Options options, - IncrementalDecoder.TextStream incrementalDecoder, ErrorDecoder errorDecoder, - Lazy httpExecutor) { - 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); - } - - @Override public Subscription invoke(Object[] argv) { - final AtomicBoolean subscribed = new AtomicBoolean(true); - final Object[] oneMoreArg = new Object[argv.length + 1]; - System.arraycopy(argv, 0, oneMoreArg, 0, argv.length); - oneMoreArg[argv.length] = subscribed; - httpExecutor.get().execute(new Runnable() { - @Override public void run() { - Error error = null; - Object arg = oneMoreArg[oneMoreArg.length - 2]; - Observer observer = Observer.class.cast(arg); - try { - ObserverHandler.super.invoke(oneMoreArg); - observer.onSuccess(); - } catch (Error cause) { - // assign to a variable in case .onFailure throws a RTE - error = cause; - observer.onFailure(cause); - } catch (Throwable cause) { - observer.onFailure(cause); - } finally { - Thread.currentThread().setName(IDLE_THREAD_NAME); - if (error != null) - throw error; - } - } - }); - return new Subscription() { - @Override public void unsubscribe() { - subscribed.set(false); - } - }; - } - - @Override protected Void decode(Object[] oneMoreArg, Response response) throws IOException { - Object arg = oneMoreArg[oneMoreArg.length - 2]; - Observer observer = Observer.class.cast(arg); - AtomicBoolean subscribed = AtomicBoolean.class.cast(oneMoreArg[oneMoreArg.length - 1]); - if (metadata.incrementalType().equals(Response.class)) { - observer.onNext(response); - } else if (metadata.incrementalType() != Void.class) { - Response.Body body = response.body(); - if (body == null) - return null; - Reader reader = body.asReader(); - try { - incrementalDecoder.decode(reader, metadata.incrementalType(), observer, subscribed); - } finally { - ensureClosed(body); - } - } - return null; - } - - @Override protected Request targetRequest(RequestTemplate template) { - Request request = super.targetRequest(template); - Thread.currentThread().setName(THREAD_PREFIX + metadata.configKey()); - return request; - } - } - /** * same approach as retrofit: temporarily rename threads */ static String THREAD_PREFIX = "Feign-"; static String IDLE_THREAD_NAME = THREAD_PREFIX + "Idle"; - static class SynchronousMethodHandler extends BaseMethodHandler { + static final class SynchronousMethodHandler implements MethodHandler { + + private final MethodMetadata metadata; + private final Target target; + private final Client client; + private final Provider retryer; + private final Set requestInterceptors; + private final Logger logger; + private final Provider logLevel; + private final BuildTemplateFromArgs buildTemplateFromArgs; + private final Options options; private final Decoder.TextStream decoder; + private final ErrorDecoder errorDecoder; private SynchronousMethodHandler(Target target, Client client, Provider retryer, Set requestInterceptors, Logger logger, Provider logLevel, MethodMetadata metadata, BuildTemplateFromArgs buildTemplateFromArgs, Options options, Decoder.TextStream decoder, ErrorDecoder errorDecoder) { - super(target, client, retryer, requestInterceptors, logger, logLevel, metadata, buildTemplateFromArgs, options, - errorDecoder); - this.decoder = checkNotNull(decoder, "decoder for %s", target); - } - - @Override protected Object decode(Object[] argv, Response response) throws Throwable { - if (metadata.returnType().equals(Response.class)) { - return response; - } else if (metadata.returnType() == void.class || response.body() == null) { - return null; - } - try { - return decoder.decode(response.body().asReader(), metadata.returnType()); - } catch (FeignException e) { - throw e; - } catch (RuntimeException e) { - throw new DecodeException(e.getMessage(), e); - } - } - } - - static abstract class BaseMethodHandler implements MethodHandler { - - protected final MethodMetadata metadata; - protected final Target target; - protected final Client client; - protected final Provider retryer; - protected final Set requestInterceptors; - protected final Logger logger; - protected final Provider logLevel; - protected final BuildTemplateFromArgs buildTemplateFromArgs; - protected final Options options; - protected final ErrorDecoder errorDecoder; - - private BaseMethodHandler(Target target, Client client, Provider retryer, - Set requestInterceptors, Logger logger, - Provider 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); @@ -243,6 +100,7 @@ private BaseMethodHandler(Target target, Client client, Provider ret this.buildTemplateFromArgs = checkNotNull(buildTemplateFromArgs, "metadata for %s", target); this.options = checkNotNull(options, "options for %s", target); this.errorDecoder = checkNotNull(errorDecoder, "errorDecoder for %s", target); + this.decoder = checkNotNull(decoder, "decoder for %s", target); } @Override public Object invoke(Object[] argv) throws Throwable { @@ -250,7 +108,7 @@ private BaseMethodHandler(Target target, Client client, Provider ret Retryer retryer = this.retryer.get(); while (true) { try { - return executeAndDecode(argv, template); + return executeAndDecode(template); } catch (RetryableException e) { retryer.continueOrPropagate(e); if (logLevel.get() != Logger.Level.NONE) { @@ -261,7 +119,7 @@ private BaseMethodHandler(Target target, Client client, Provider ret } } - public Object executeAndDecode(Object[] argv, RequestTemplate template) throws Throwable { + Object executeAndDecode(RequestTemplate template) throws Throwable { Request request = targetRequest(template); if (logLevel.get() != Logger.Level.NONE) { @@ -285,7 +143,7 @@ public Object executeAndDecode(Object[] argv, RequestTemplate template) throws T response = logger.logAndRebufferResponse(metadata.configKey(), logLevel.get(), response, elapsedTime); } if (response.status() >= 200 && response.status() < 300) { - return decode(argv, response); + return decode(response); } else { throw errorDecoder.decode(metadata.configKey(), response); } @@ -299,17 +157,30 @@ public Object executeAndDecode(Object[] argv, RequestTemplate template) throws T } } - protected long elapsedTime(long start) { + long elapsedTime(long start) { return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); } - protected Request targetRequest(RequestTemplate template) { + Request targetRequest(RequestTemplate template) { for (RequestInterceptor interceptor : requestInterceptors) { interceptor.apply(template); } return target.apply(new RequestTemplate(template)); } - protected abstract Object decode(Object[] argv, Response response) throws Throwable; + Object decode(Response response) throws Throwable { + if (metadata.returnType().equals(Response.class)) { + return response; + } else if (metadata.returnType() == void.class || response.body() == null) { + return null; + } + try { + return decoder.decode(response.body().asReader(), metadata.returnType()); + } catch (FeignException e) { + throw e; + } catch (RuntimeException e) { + throw new DecodeException(e.getMessage(), e); + } + } } } diff --git a/core/src/main/java/feign/MethodMetadata.java b/core/src/main/java/feign/MethodMetadata.java index 14ca1f1a33..d2c8f3a5d2 100644 --- a/core/src/main/java/feign/MethodMetadata.java +++ b/core/src/main/java/feign/MethodMetadata.java @@ -30,9 +30,7 @@ public final class MethodMetadata implements Serializable { private String configKey; private transient Type returnType; - private transient Type incrementalType; private Integer urlIndex; - private Integer observerIndex; private Integer bodyIndex; private transient Type bodyType; private RequestTemplate template = new RequestTemplate(); @@ -63,19 +61,6 @@ MethodMetadata returnType(Type returnType) { return this; } - /** - * Type that {@link feign.codec.IncrementalDecoder} must process. If null, - * {@link feign.codec.Decoder} will be used against the {@link #returnType()}; - */ - public Type incrementalType() { - return incrementalType; - } - - MethodMetadata incrementalType(Type incrementalType) { - this.incrementalType = incrementalType; - return this; - } - public Integer urlIndex() { return urlIndex; } diff --git a/core/src/main/java/feign/Observable.java b/core/src/main/java/feign/Observable.java deleted file mode 100644 index 0ea6112e84..0000000000 --- a/core/src/main/java/feign/Observable.java +++ /dev/null @@ -1,39 +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; - -/** - * An {@code Observer} is asynchronous equivalent to an {@code Iterable}. - *
- * Each call to {@link #subscribe(Observer)} implies a new - * {@link Request HTTP request}. - * - * @param expected value to decode incrementally from the http response. - */ -public interface Observable { - - /** - * Calling subscribe will initiate a new HTTP request which will be - * {@link feign.codec.IncrementalDecoder incrementally decoded} into the - * {@code observer} until it is finished or - * {@link feign.Subscription#unsubscribe()} is called. - * - * @param observer - * @return a {@link Subscription} with which you can stop the streaming of - * events to the {@code observer}. - */ - public Subscription subscribe(Observer observer); -} diff --git a/core/src/main/java/feign/Observer.java b/core/src/main/java/feign/Observer.java deleted file mode 100644 index d0aa6c78c4..0000000000 --- a/core/src/main/java/feign/Observer.java +++ /dev/null @@ -1,68 +0,0 @@ -package feign; - -/** - * An {@code Observer} is asynchronous equivalent to an {@code Iterator}. - *

- * Observers receive results as they are - * {@link feign.codec.IncrementalDecoder decoded} from an - * {@link Response.Body http response body}. {@link #onNext(Object) onNext} - * will be called for each incremental value of type {@code T} until - * {@link feign.Subscription#unsubscribe()} is called or the response is finished. - *
- * {@link #onSuccess() onSuccess} or {@link #onFailure(Throwable)} onFailure} - * will be called when the response is finished, but not both. - *
- * {@code Observer} can be used as an asynchronous alternative to a - * {@code Collection}, or any other use where iterative response parsing is - * worth the additional effort to implement this interface. - *
- *
- * Here's an example of implementing {@code Observer}: - *
- *

- * Observer counter = new Observer() {
- *
- *   public int count;
- *
- *   @Override public void onNext(Contributor element) {
- *     count++;
- *   }
- *
- *   @Override public void onSuccess() {
- *     System.out.println("found " + count + " contributors");
- *   }
- *
- *   @Override public void onFailure(Throwable cause) {
- *     System.err.println("sad face after contributor " + count);
- *   }
- * };
- * subscription = github.contributors("netflix", "feign", counter);
- * 
- * - * @param expected value to decode incrementally from the http response. - */ -public interface Observer { - /** - * Invoked as soon as new data is available. Could be invoked many times or - * not at all. - * - * @param element next decoded element. - */ - void onNext(T element); - - /** - * Called when response processing completed successfully. - */ - void onSuccess(); - - /** - * Called when response processing failed for any reason. - *
- * Common failure cases include {@link FeignException}, - * {@link java.io.IOException}, and {@link feign.codec.DecodeException}. - * However, the cause could be a {@code Throwable} of any kind. - * - * @param cause the reason for the failure - */ - void onFailure(Throwable cause); -} diff --git a/core/src/main/java/feign/ReflectiveFeign.java b/core/src/main/java/feign/ReflectiveFeign.java index 0cb2490caa..81029285d7 100644 --- a/core/src/main/java/feign/ReflectiveFeign.java +++ b/core/src/main/java/feign/ReflectiveFeign.java @@ -15,19 +15,15 @@ */ package feign; -import dagger.Lazy; import dagger.Provides; import feign.Request.Options; import feign.codec.Decoder; import feign.codec.EncodeException; import feign.codec.Encoder; import feign.codec.ErrorDecoder; -import feign.codec.IncrementalDecoder; import feign.codec.StringDecoder; -import feign.codec.StringIncrementalDecoder; import javax.inject.Inject; -import javax.inject.Named; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; @@ -40,7 +36,6 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; -import java.util.concurrent.Executor; import static feign.Util.checkArgument; import static feign.Util.checkNotNull; @@ -53,8 +48,7 @@ public class ReflectiveFeign extends Feign { private final ParseHandlersByName targetToHandlersByName; - @Inject ReflectiveFeign(@Named("http") Lazy httpExecutor, ParseHandlersByName targetToHandlersByName) { - super(httpExecutor); + @Inject ReflectiveFeign(ParseHandlersByName targetToHandlersByName) { this.targetToHandlersByName = targetToHandlersByName; } @@ -136,10 +130,6 @@ public static class Module { return Collections.emptySet(); } - @Provides(type = Provides.Type.SET_VALUES) Set noIncrementalDecoders() { - return Collections.emptySet(); - } - @Provides Feign provideFeign(ReflectiveFeign in) { return in; } @@ -151,15 +141,12 @@ static final class ParseHandlersByName { private final Map> encoders = new HashMap>(); private final Encoder.Text> formEncoder; private final Map> decoders = new HashMap>(); - private final Map> incrementalDecoders = - new HashMap>(); private final ErrorDecoder errorDecoder; private final MethodHandler.Factory factory; @SuppressWarnings("unchecked") @Inject ParseHandlersByName(Contract contract, Options options, Set encoders, Set decoders, - Set incrementalDecoders, ErrorDecoder errorDecoder, - MethodHandler.Factory factory) { + ErrorDecoder errorDecoder, MethodHandler.Factory factory) { this.contract = contract; this.options = options; this.factory = factory; @@ -191,16 +178,6 @@ static final class ParseHandlersByName { Type type = resolveLastTypeParameter(decoder.getClass(), Decoder.class); this.decoders.put(type, Decoder.TextStream.class.cast(decoder)); } - StringIncrementalDecoder stringIncrementalDecoder = new StringIncrementalDecoder(); - this.incrementalDecoders.put(Void.class, stringIncrementalDecoder); - this.incrementalDecoders.put(Response.class, stringIncrementalDecoder); - this.incrementalDecoders.put(String.class, stringIncrementalDecoder); - for (IncrementalDecoder incrementalDecoder : incrementalDecoders) { - checkState(incrementalDecoder instanceof IncrementalDecoder.TextStream, - "Currently, only IncrementalDecoder.TextStream is supported. Found: ", incrementalDecoder); - Type type = resolveLastTypeParameter(incrementalDecoder.getClass(), IncrementalDecoder.class); - this.incrementalDecoders.put(type, IncrementalDecoder.TextStream.class.cast(incrementalDecoder)); - } } public Map apply(Target key) { @@ -227,27 +204,15 @@ public Map apply(Target key) { } else { buildTemplate = new BuildTemplateByResolvingArgs(md); } - if (md.incrementalType() != null) { - IncrementalDecoder.TextStream incrementalDecoder = incrementalDecoders.get(md.incrementalType()); - if (incrementalDecoder == null) { - incrementalDecoder = incrementalDecoders.get(Object.class); - } - if (incrementalDecoder == null) { - throw new IllegalStateException(format("%s needs @Provides(type = Set) IncrementalDecoder incrementalDecoder()" + - "{ // IncrementalDecoder.TextStream<%s> or IncrementalDecoder.TextStream}", md.configKey(), md.incrementalType())); - } - result.put(md.configKey(), factory.create(key, md, buildTemplate, options, incrementalDecoder, errorDecoder)); - } else { - Decoder.TextStream decoder = decoders.get(md.returnType()); - if (decoder == null) { - decoder = decoders.get(Object.class); - } - if (decoder == null) { - throw new IllegalStateException(format("%s needs @Provides(type = Set) Decoder decoder()" + - "{ // Decoder.TextStream<%s> or Decoder.TextStream}", md.configKey(), md.returnType())); - } - result.put(md.configKey(), factory.create(key, md, buildTemplate, options, decoder, errorDecoder)); + Decoder.TextStream decoder = decoders.get(md.returnType()); + if (decoder == null) { + decoder = decoders.get(Object.class); + } + if (decoder == null) { + throw new IllegalStateException(format("%s needs @Provides(type = Set) Decoder decoder()" + + "{ // Decoder.TextStream<%s> or Decoder.TextStream}", md.configKey(), md.returnType())); } + result.put(md.configKey(), factory.create(key, md, buildTemplate, options, decoder, errorDecoder)); } return result; } diff --git a/core/src/main/java/feign/Subscription.java b/core/src/main/java/feign/Subscription.java deleted file mode 100644 index 1b327f747b..0000000000 --- a/core/src/main/java/feign/Subscription.java +++ /dev/null @@ -1,32 +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; - -/** - * Subscription returns from {@link Observable#subscribe(Observer)} to allow - * unsubscribing. - */ -public interface Subscription { - - /** - * Stop receiving notifications on the {@link Observer} that was registered - * when this Subscription was received. - *
- * This allows unregistering an {@link Observer} before it has finished - * receiving all events (ie. before onCompleted is called). - */ - void unsubscribe(); -} diff --git a/core/src/main/java/feign/codec/IncrementalDecoder.java b/core/src/main/java/feign/codec/IncrementalDecoder.java deleted file mode 100644 index 00e11b4b2d..0000000000 --- a/core/src/main/java/feign/codec/IncrementalDecoder.java +++ /dev/null @@ -1,117 +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.codec; - -import feign.FeignException; -import feign.Observer; - -import java.io.IOException; -import java.io.Reader; -import java.lang.reflect.Type; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * Decodes an HTTP response incrementally into an {@link feign.Observer} - * via a series of {@link feign.Observer#onNext(Object) onNext} calls. - *

- * Invoked when {@link feign.Response#status()} is in the 2xx range. - * - * @param input that can be derived from {@link feign.Response.Body}. - * @param widest type an instance of this can decode. - */ -public interface IncrementalDecoder { - /** - * Implement this to decode a resource to an object into a single object. - * If you need to wrap exceptions, please do so via {@link feign.codec.DecodeException}. - *
- * Do not call {@link feign.Observer#onSuccess() onSuccess} or - * {@link feign.Observer#onFailure onFailure}. - * - * @param input if {@code Closeable}, no need to close this, as the caller - * manages resources. - * @param type type parameter of {@link feign.Observer#onNext}. - * @param observer call {@link feign.Observer#onNext onNext} - * each time an object of {@code type} is decoded - * from the response. - * @param subscribed false indicates the observer should no longer receive - * {@link Observer#onNext(Object)} calls. - * @throws java.io.IOException will be propagated safely to the caller. - * @throws feign.codec.DecodeException when decoding failed due to a checked exception - * besides IOException. - * @throws feign.FeignException when decoding succeeds, but conveys the operation - * failed. - */ - void decode(I input, Type type, Observer observer, AtomicBoolean subscribed) - throws IOException, DecodeException, FeignException; - - /** - * Used for text-based apis, follows - * {@link feign.codec.IncrementalDecoder#decode(Object, java.lang.reflect.Type, feign.Observer, AtomicBoolean)} - * semantics, applied to inputs of type {@link java.io.Reader}.
- * Ex.
- *

- *

-   * public class GsonDecoder implements Decoder.TextStream<Object> {
-   *   private final Gson gson;
-   *
-   *   public GsonDecoder(Gson gson) {
-   *     this.gson = gson;
-   *   }
-   *
-   *   @Override
-   *   public Object decode(Reader reader, Type type) throws IOException {
-   *     try {
-   *       return gson.fromJson(reader, type);
-   *     } catch (JsonIOException e) {
-   *       if (e.getCause() != null &&
-   *           e.getCause() instanceof IOException) {
-   *         throw IOException.class.cast(e.getCause());
-   *       }
-   *       throw e;
-   *     }
-   *   }
-   * }
-   * 
- *
-   * public class GsonIncrementalDecoder implements IncrementalDecoder {
-   *   private final Gson gson;
-   *
-   *   public GsonIncrementalDecoder(Gson gson) {
-   *     this.gson = gson;
-   *   }
-   *
-   *   @Override public void decode(Reader reader, Type type, Observer observer) throws Exception {
-   *     JsonReader jsonReader = new JsonReader(reader);
-   *     jsonReader.beginArray();
-   *     while (jsonReader.hasNext()) {
-   *       try {
-   *          observer.onNext(gson.fromJson(jsonReader, type));
-   *       } catch (JsonIOException e) {
-   *         if (e.getCause() != null &&
-   *             e.getCause() instanceof IOException) {
-   *           throw IOException.class.cast(e.getCause());
-   *         }
-   *         throw e;
-   *       }
-   *     }
-   *     jsonReader.endArray();
-   *   }
-   * }
-   * 
-   */
-  public interface TextStream extends IncrementalDecoder {
-  }
-}
diff --git a/core/src/main/java/feign/codec/StringIncrementalDecoder.java b/core/src/main/java/feign/codec/StringIncrementalDecoder.java
deleted file mode 100644
index a3fa77bae8..0000000000
--- a/core/src/main/java/feign/codec/StringIncrementalDecoder.java
+++ /dev/null
@@ -1,33 +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.codec;
-
-import feign.Observer;
-
-import java.io.IOException;
-import java.io.Reader;
-import java.lang.reflect.Type;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-public class StringIncrementalDecoder implements IncrementalDecoder.TextStream {
-  private static final StringDecoder STRING_DECODER = new StringDecoder();
-
-  @Override
-  public void decode(Reader reader, Type type, Observer observer, AtomicBoolean subscribed)
-      throws IOException {
-    observer.onNext(STRING_DECODER.decode(reader, type));
-  }
-}
diff --git a/core/src/test/java/feign/DefaultContractTest.java b/core/src/test/java/feign/DefaultContractTest.java
index aaaaf7ebc4..7dae475857 100644
--- a/core/src/test/java/feign/DefaultContractTest.java
+++ b/core/src/test/java/feign/DefaultContractTest.java
@@ -21,7 +21,6 @@
 import org.testng.annotations.Test;
 
 import javax.inject.Named;
-import java.lang.reflect.Type;
 import java.net.URI;
 import java.util.List;
 
@@ -238,40 +237,4 @@ interface HeaderParams {
     assertEquals(md.template().headers().get("Auth-Token"), ImmutableSet.of("{Auth-Token}"));
     assertEquals(md.indexToName().get(0), ImmutableSet.of("Auth-Token"));
   }
-
-  interface WithObservable {
-    @RequestLine("GET /") Observable> valid();
-
-    @RequestLine("GET /") Observable> wildcardExtends();
-
-    @RequestLine("GET /") ParameterizedObservable> subtype();
-
-    @RequestLine("GET /") Response returnType(Observable> one);
-
-    @RequestLine("GET /") Observable> alsoObserver(Observer> observer);
-  }
-
-  interface ParameterizedObservable> extends Observable {
-  }
-
-  static final List listString = null;
-
-  @Test public void methodCanHaveObservableReturn() throws Exception {
-    contract.parseAndValidatateMetadata(WithObservable.class.getDeclaredMethod("valid"));
-  }
-
-  @Test public void methodMetadataReturnTypeOnObservableMethodIsItsTypeParameter() throws Exception {
-    Type listStringType = getClass().getDeclaredField("listString").getGenericType();
-    MethodMetadata md = contract.parseAndValidatateMetadata(WithObservable.class.getDeclaredMethod("valid"));
-    assertEquals(md.incrementalType(), listStringType);
-    md = contract.parseAndValidatateMetadata(WithObservable.class.getDeclaredMethod("wildcardExtends"));
-    assertEquals(md.incrementalType(), listStringType);
-    md = contract.parseAndValidatateMetadata(WithObservable.class.getDeclaredMethod("subtype"));
-    assertEquals(md.incrementalType(), listStringType);
-  }
-
-  @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "Please return Observer as opposed to passing an Observable arg.*")
-  public void noObserverArgs() throws Exception {
-    contract.parseAndValidatateMetadata(WithObservable.class.getDeclaredMethod("alsoObserver", Observer.class));
-  }
 }
diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java
index e604d5dc01..2c2b7b90eb 100644
--- a/core/src/test/java/feign/FeignTest.java
+++ b/core/src/test/java/feign/FeignTest.java
@@ -22,7 +22,6 @@
 import com.google.mockwebserver.MockWebServer;
 import com.google.mockwebserver.RecordedRequest;
 import com.google.mockwebserver.SocketPolicy;
-import dagger.Lazy;
 import dagger.Module;
 import dagger.Provides;
 import feign.codec.Decoder;
@@ -42,11 +41,7 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.atomic.AtomicBoolean;
 
 import static dagger.Provides.Type.SET;
 import static feign.Util.UTF_8;
@@ -54,27 +49,12 @@
 import static org.testng.Assert.assertFalse;
 import static org.testng.Assert.assertNull;
 import static org.testng.Assert.assertTrue;
-import static org.testng.Assert.fail;
 
 @Test
 // unbound wildcards are not currently injectable in dagger.
 @SuppressWarnings("rawtypes")
 public class FeignTest {
 
-  @Test public void closeShutsdownExecutorService() throws IOException, InterruptedException {
-    final ExecutorService service = Executors.newCachedThreadPool();
-    new Feign(new Lazy() {
-      @Override public Executor get() {
-        return service;
-      }
-    }) {
-      @Override public  T newInstance(Target target) {
-        return null;
-      }
-    }.close();
-    assertTrue(service.isShutdown());
-  }
-
   interface TestInterface {
     @RequestLine("POST /") String post();
 
@@ -94,12 +74,6 @@ void login(
 
     @RequestLine("GET /?1={1}&2={2}") Response queryParams(@Named("1") String one, @Named("2") Iterable twos);
 
-    @RequestLine("POST /") Observable observableVoid();
-
-    @RequestLine("POST /") Observable observableString();
-
-    @RequestLine("POST /") Observable observableResponse();
-
     @dagger.Module(library = true)
     static class Module {
       @Provides(type = SET) Encoder defaultEncoder() {
@@ -140,76 +114,6 @@ interface OtherTestInterface {
     @RequestLine("POST /") String post();
   }
 
-  @Test
-  public void observableVoid() 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 RunSynchronous());
-
-      final AtomicBoolean success = new AtomicBoolean();
-
-      Observer observer = new Observer() {
-
-        @Override public void onNext(Void element) {
-          fail("on next isn't valid for void");
-        }
-
-        @Override public void onSuccess() {
-          success.set(true);
-        }
-
-        @Override public void onFailure(Throwable cause) {
-          fail(cause.getMessage());
-        }
-      };
-      api.observableVoid().subscribe(observer);
-
-      assertTrue(success.get());
-      assertEquals(server.getRequestCount(), 1);
-    } finally {
-      server.shutdown();
-    }
-  }
-
-  @Test
-  public void observableResponse() 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 RunSynchronous());
-
-      final AtomicBoolean success = new AtomicBoolean();
-
-      Observer observer = new Observer() {
-
-        @Override public void onNext(Response element) {
-          assertEquals(element.status(), 200);
-        }
-
-        @Override public void onSuccess() {
-          success.set(true);
-        }
-
-        @Override public void onFailure(Throwable cause) {
-          fail(cause.getMessage());
-        }
-      };
-      api.observableResponse().subscribe(observer);
-
-      assertTrue(success.get());
-      assertEquals(server.getRequestCount(), 1);
-    } finally {
-      server.shutdown();
-    }
-  }
-
   @Module(library = true, overrides = true)
   static class RunSynchronous {
     @Provides @Singleton @Named("http") Executor httpExecutor() {
@@ -221,79 +125,6 @@ static class RunSynchronous {
     }
   }
 
-  @Test
-  public void incrementString() 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 RunSynchronous());
-
-      final AtomicBoolean success = new AtomicBoolean();
-
-      Observer observer = new Observer() {
-
-        @Override public void onNext(String element) {
-          assertEquals(element, "foo");
-        }
-
-        @Override public void onSuccess() {
-          success.set(true);
-        }
-
-        @Override public void onFailure(Throwable cause) {
-          fail(cause.getMessage());
-        }
-      };
-      api.observableString().subscribe(observer);
-
-      assertTrue(success.get());
-      assertEquals(server.getRequestCount(), 1);
-    } finally {
-      server.shutdown();
-    }
-  }
-
-  @Test
-  public void multipleObservers() throws IOException, InterruptedException {
-    final MockWebServer server = new MockWebServer();
-    server.enqueue(new MockResponse().setBody("foo"));
-    server.enqueue(new MockResponse().setBody("foo"));
-    server.play();
-
-    try {
-      TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module());
-
-      final CountDownLatch latch = new CountDownLatch(2);
-
-      Observer observer = new Observer() {
-
-        @Override public void onNext(String element) {
-          assertEquals(element, "foo");
-        }
-
-        @Override public void onSuccess() {
-          latch.countDown();
-        }
-
-        @Override public void onFailure(Throwable cause) {
-          fail(cause.getMessage());
-        }
-      };
-
-      Observable observable = api.observableString();
-      observable.subscribe(observer);
-      observable.subscribe(observer);
-      latch.await();
-
-      assertEquals(server.getRequestCount(), 2);
-    } finally {
-      server.shutdown();
-    }
-  }
-
   @Test
   public void postTemplateParamsResolve() throws IOException, InterruptedException {
     final MockWebServer server = new MockWebServer();
diff --git a/core/src/test/java/feign/examples/GitHubExample.java b/core/src/test/java/feign/examples/GitHubExample.java
index 428f968570..aaac375221 100644
--- a/core/src/test/java/feign/examples/GitHubExample.java
+++ b/core/src/test/java/feign/examples/GitHubExample.java
@@ -22,11 +22,8 @@
 import dagger.Provides;
 import feign.Feign;
 import feign.Logger;
-import feign.Observable;
-import feign.Observer;
 import feign.RequestLine;
 import feign.codec.Decoder;
-import feign.codec.IncrementalDecoder;
 
 import javax.inject.Inject;
 import javax.inject.Named;
@@ -35,8 +32,6 @@
 import java.io.Reader;
 import java.lang.reflect.Type;
 import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.atomic.AtomicBoolean;
 
 import static dagger.Provides.Type.SET;
 
@@ -48,9 +43,6 @@ public class GitHubExample {
   interface GitHub {
     @RequestLine("GET /repos/{owner}/{repo}/contributors")
     List contributors(@Named("owner") String owner, @Named("repo") String repo);
-
-    @RequestLine("GET /repos/{owner}/{repo}/contributors")
-    Observable observable(@Named("owner") String owner, @Named("repo") String repo);
   }
 
   static class Contributor {
@@ -66,20 +58,6 @@ public static void main(String... args) throws InterruptedException {
     for (Contributor contributor : contributors) {
       System.out.println(contributor.login + " (" + contributor.contributions + ")");
     }
-
-    System.out.println("Let's treat our contributors as an observable.");
-    Observable observable = github.observable("netflix", "feign");
-
-    CountDownLatch latch = new CountDownLatch(2);
-
-    System.out.println("Let's add 2 subscribers.");
-    observable.subscribe(new ContributorObserver(latch));
-    observable.subscribe(new ContributorObserver(latch));
-
-    // wait for the task to complete.
-    latch.await();
-
-    System.exit(0);
   }
 
   @Module(overrides = true, library = true, includes = GsonModule.class)
@@ -107,13 +85,9 @@ static class GsonModule {
     @Provides(type = SET) Decoder decoder(GsonDecoder gsonDecoder) {
       return gsonDecoder;
     }
-
-    @Provides(type = SET) IncrementalDecoder incrementalDecoder(GsonDecoder gsonDecoder) {
-      return gsonDecoder;
-    }
   }
 
-  static class GsonDecoder implements Decoder.TextStream, IncrementalDecoder.TextStream {
+  static class GsonDecoder implements Decoder.TextStream {
     private final Gson gson;
 
     @Inject GsonDecoder(Gson gson) {
@@ -124,15 +98,6 @@ static class GsonDecoder implements Decoder.TextStream, IncrementalDecod
       return fromJson(new JsonReader(reader), type);
     }
 
-    @Override
-    public void decode(Reader reader, Type type, Observer observer, AtomicBoolean subscribed) throws IOException {
-      JsonReader jsonReader = new JsonReader(reader);
-      jsonReader.beginArray();
-      while (jsonReader.hasNext() && subscribed.get()) {
-        observer.onNext(fromJson(jsonReader, type));
-      }
-    }
-
     private Object fromJson(JsonReader jsonReader, Type type) throws IOException {
       try {
         return gson.fromJson(jsonReader, type);
@@ -144,29 +109,4 @@ private Object fromJson(JsonReader jsonReader, Type type) throws IOException {
       }
     }
   }
-
-  static class ContributorObserver implements Observer {
-
-    private final CountDownLatch latch;
-    public int count;
-
-    public ContributorObserver(CountDownLatch latch) {
-      this.latch = latch;
-    }
-
-    // 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();
-    }
-  }
 }
diff --git a/gson/src/main/java/feign/gson/GsonModule.java b/gson/src/main/java/feign/gson/GsonModule.java
index aab32687d3..52fd8077d3 100644
--- a/gson/src/main/java/feign/gson/GsonModule.java
+++ b/gson/src/main/java/feign/gson/GsonModule.java
@@ -26,11 +26,9 @@
 import com.google.gson.stream.JsonReader;
 import com.google.gson.stream.JsonWriter;
 import dagger.Provides;
-import feign.Observer;
 import feign.codec.Decoder;
 import feign.codec.EncodeException;
 import feign.codec.Encoder;
-import feign.codec.IncrementalDecoder;
 
 import javax.inject.Inject;
 import javax.inject.Singleton;
@@ -39,7 +37,6 @@
 import java.lang.reflect.Type;
 import java.util.Collections;
 import java.util.Map;
-import java.util.concurrent.atomic.AtomicBoolean;
 
 import static dagger.Provides.Type.SET;
 
@@ -54,11 +51,7 @@ public final class GsonModule {
     return codec;
   }
 
-  @Provides(type = SET) IncrementalDecoder incrementalDecoder(GsonCodec codec) {
-    return codec;
-  }
-
-  static class GsonCodec implements Encoder.Text, Decoder.TextStream, IncrementalDecoder.TextStream {
+  static class GsonCodec implements Encoder.Text, Decoder.TextStream {
     private final Gson gson;
 
     @Inject GsonCodec(Gson gson) {
@@ -73,15 +66,6 @@ static class GsonCodec implements Encoder.Text, Decoder.TextStream observer, AtomicBoolean subscribed) throws IOException {
-      JsonReader jsonReader = new JsonReader(reader);
-      jsonReader.beginArray();
-      while (subscribed.get() && jsonReader.hasNext()) {
-        observer.onNext(fromJson(jsonReader, type));
-      }
-    }
-
     private Object fromJson(JsonReader jsonReader, Type type) throws IOException {
       try {
         return gson.fromJson(jsonReader, type);
diff --git a/gson/src/test/java/feign/gson/GsonModuleTest.java b/gson/src/test/java/feign/gson/GsonModuleTest.java
index 983c58ffcf..bde0f8d71d 100644
--- a/gson/src/test/java/feign/gson/GsonModuleTest.java
+++ b/gson/src/test/java/feign/gson/GsonModuleTest.java
@@ -18,10 +18,8 @@
 import com.google.gson.reflect.TypeToken;
 import dagger.Module;
 import dagger.ObjectGraph;
-import feign.Observer;
 import feign.codec.Decoder;
 import feign.codec.Encoder;
-import feign.codec.IncrementalDecoder;
 import org.testng.annotations.Test;
 
 import javax.inject.Inject;
@@ -32,11 +30,8 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
 
 import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.fail;
 
 @Test
 public class GsonModuleTest {
@@ -44,7 +39,6 @@ public class GsonModuleTest {
   static class EncodersAndDecoders {
     @Inject Set encoders;
     @Inject Set decoders;
-    @Inject Set incrementalDecoders;
   }
 
   @Test public void providesEncoderDecoderAndIncrementalDecoder() throws Exception {
@@ -55,8 +49,6 @@ static class EncodersAndDecoders {
     assertEquals(bindings.encoders.iterator().next().getClass(), GsonModule.GsonCodec.class);
     assertEquals(bindings.decoders.size(), 1);
     assertEquals(bindings.decoders.iterator().next().getClass(), GsonModule.GsonCodec.class);
-    assertEquals(bindings.incrementalDecoders.size(), 1);
-    assertEquals(bindings.incrementalDecoders.iterator().next().getClass(), GsonModule.GsonCodec.class);
   }
 
   @Module(includes = GsonModule.class, library = true, injects = Encoders.class)
@@ -132,44 +124,6 @@ static class Decoders {
         }.getType()), zones);
   }
 
-  @Module(includes = GsonModule.class, library = true, injects = IncrementalDecoders.class)
-  static class IncrementalDecoders {
-    @Inject Set decoders;
-  }
-
-  @Test public void decodesIncrementally() throws Exception {
-    IncrementalDecoders bindings = new IncrementalDecoders();
-    ObjectGraph.create(bindings).inject(bindings);
-
-    final List zones = new LinkedList();
-    zones.add(new Zone("denominator.io."));
-    zones.add(new Zone("denominator.io.", "ABCD"));
-
-    final AtomicInteger index = new AtomicInteger(0);
-
-    Observer zoneCallback = new Observer() {
-
-      @Override public void onNext(Zone element) {
-        assertEquals(element, zones.get(index.getAndIncrement()));
-      }
-
-      @Override public void onSuccess() {
-        // decoder shouldn't call onSuccess
-        fail();
-      }
-
-      @Override public void onFailure(Throwable cause) {
-        // decoder shouldn't call onFailure
-        fail();
-      }
-    };
-
-    IncrementalDecoder.TextStream.class.cast(bindings.decoders.iterator().next())
-        .decode(new StringReader(zonesJson), Zone.class, zoneCallback, new AtomicBoolean(true));
-
-    assertEquals(index.get(), 2);
-  }
-
   private String zonesJson = ""//
       + "[\n"//
       + "  {\n"//
diff --git a/jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java b/jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java
index 1669e3698c..7a573e00a4 100644
--- a/jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java
+++ b/jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java
@@ -19,8 +19,6 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.gson.reflect.TypeToken;
 import feign.MethodMetadata;
-import feign.Observable;
-import feign.Observer;
 import feign.Response;
 import org.testng.annotations.Test;
 
@@ -40,7 +38,6 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
-import java.lang.reflect.Type;
 import java.net.URI;
 import java.util.List;
 
@@ -345,38 +342,4 @@ interface HeaderParams {
   public void emptyHeaderParam() throws Exception {
     contract.parseAndValidatateMetadata(HeaderParams.class.getDeclaredMethod("emptyHeaderParam", String.class));
   }
-
-  interface WithObservable {
-    @GET @Path("/") Observable> valid();
-
-    @GET @Path("/") Observable> wildcardExtends();
-
-    @GET @Path("/") ParameterizedObservable> subtype();
-
-    @GET @Path("/") Observable> alsoObserver(Observer> observer);
-  }
-
-  interface ParameterizedObservable> extends Observable {
-  }
-
-  static final List listString = null;
-
-  @Test public void methodCanHaveObservableReturn() throws Exception {
-    contract.parseAndValidatateMetadata(WithObservable.class.getDeclaredMethod("valid"));
-  }
-
-  @Test public void methodMetadataReturnTypeOnObservableMethodIsItsTypeParameter() throws Exception {
-    Type listStringType = getClass().getDeclaredField("listString").getGenericType();
-    MethodMetadata md = contract.parseAndValidatateMetadata(WithObservable.class.getDeclaredMethod("valid"));
-    assertEquals(md.incrementalType(), listStringType);
-    md = contract.parseAndValidatateMetadata(WithObservable.class.getDeclaredMethod("wildcardExtends"));
-    assertEquals(md.incrementalType(), listStringType);
-    md = contract.parseAndValidatateMetadata(WithObservable.class.getDeclaredMethod("subtype"));
-    assertEquals(md.incrementalType(), listStringType);
-  }
-
-  @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "Please return Observer as opposed to passing an Observable arg.*")
-  public void noObserverArgs() throws Exception {
-    contract.parseAndValidatateMetadata(WithObservable.class.getDeclaredMethod("alsoObserver", Observer.class));
-  }
 }
diff --git a/jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java b/jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java
index 80289f112a..5e99424460 100644
--- a/jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java
+++ b/jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java
@@ -19,17 +19,13 @@
 import dagger.Provides;
 import feign.Feign;
 import feign.Logger;
-import feign.Observable;
-import feign.Observer;
 import feign.gson.GsonModule;
 import feign.jaxrs.JAXRSModule;
 
-import javax.inject.Named;
 import javax.ws.rs.GET;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import java.util.List;
-import java.util.concurrent.CountDownLatch;
 
 /**
  * adapted from {@code com.example.retrofit.GitHubClient}
@@ -39,9 +35,6 @@ public class GitHubExample {
   interface GitHub {
     @GET @Path("/repos/{owner}/{repo}/contributors")
     List contributors(@PathParam("owner") String owner, @PathParam("repo") String repo);
-
-    @GET @Path("/repos/{owner}/{repo}/contributors")
-    Observable observable(@PathParam("owner") String owner, @PathParam("repo") String repo);
   }
 
   static class Contributor {
@@ -57,20 +50,6 @@ public static void main(String... args) throws InterruptedException {
     for (Contributor contributor : contributors) {
       System.out.println(contributor.login + " (" + contributor.contributions + ")");
     }
-
-    System.out.println("Let's treat our contributors as an observable.");
-    Observable observable = github.observable("netflix", "feign");
-
-    CountDownLatch latch = new CountDownLatch(2);
-
-    System.out.println("Let's add 2 subscribers.");
-    observable.subscribe(new ContributorObserver(latch));
-    observable.subscribe(new ContributorObserver(latch));
-
-    // wait for the task to complete.
-    latch.await();
-
-    System.exit(0);
   }
 
   /**
@@ -87,29 +66,4 @@ static class GitHubModule {
       return new Logger.ErrorLogger();
     }
   }
-
-  static class ContributorObserver implements Observer {
-
-    private final CountDownLatch latch;
-    public int count;
-
-    public ContributorObserver(CountDownLatch latch) {
-      this.latch = latch;
-    }
-
-    // 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();
-    }
-  }
 }

From 0ac4f9abea7b0a60d966e8451515ce0e4ebf1558 Mon Sep 17 00:00:00 2001
From: adriancole 
Date: Wed, 11 Sep 2013 22:03:46 +0200
Subject: [PATCH 098/179] SaxDecoder now decodes multiple types.

---
 CHANGES.md                                    |   1 +
 .../src/main/java/feign/codec/SAXDecoder.java |  67 ++++++---
 .../test/java/feign/codec/SAXDecoderTest.java | 136 ++++++++++++++++++
 .../test/java/feign/examples/IAMExample.java  |  65 ++++++++-
 4 files changed, 244 insertions(+), 25 deletions(-)
 create mode 100644 core/src/test/java/feign/codec/SAXDecoderTest.java

diff --git a/CHANGES.md b/CHANGES.md
index 3cf46c2c1f..7743e87859 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,6 @@
 ### Version 5.0
 * Remove support for Observable methods.
+* SaxDecoder now decodes multiple types.
 
 ### Version 4.4.1
 * Fix NullPointerException on calling equals and hashCode.
diff --git a/core/src/main/java/feign/codec/SAXDecoder.java b/core/src/main/java/feign/codec/SAXDecoder.java
index 972fee9cc2..48481adb7e 100644
--- a/core/src/main/java/feign/codec/SAXDecoder.java
+++ b/core/src/main/java/feign/codec/SAXDecoder.java
@@ -25,11 +25,52 @@
 import java.io.IOException;
 import java.io.Reader;
 import java.lang.reflect.Type;
+import java.util.LinkedHashMap;
+import java.util.Map;
 
 import static feign.Util.checkNotNull;
 import static feign.Util.checkState;
+import static feign.Util.resolveLastTypeParameter;
+
+/**
+ * Decodes responses using SAX. Configure using the {@link SAXDecoder.Builder
+ * builder}.
+ * 

+ * + *

+ * @Provides(type = SET)
+ * Decoder saxDecoder(Provider<ContentHandlerForFoo> foo, //
+ *         Provider<ContentHandlerForBar> bar) {
+ *     return SAXDecoder.builder() //
+ *             .addContentHandler(foo) //
+ *             .addContentHandler(bar) //
+ *             .build();
+ * }
+ * 
+ */ +public class SAXDecoder implements Decoder.TextStream { + + public static Builder builder() { + return new Builder(); + } + + // builder as dagger doesn't support wildcard bindings, map bindings, or set bindings of providers. + public static class Builder { + private final Map>> handlerProviders = + new LinkedHashMap>>(); + + public Builder addContentHandler(Provider> handler) { + Type type = resolveLastTypeParameter(checkNotNull(handler, "handler").getClass(), Provider.class); + type = resolveLastTypeParameter(type, ContentHandlerWithResult.class); + this.handlerProviders.put(type, handler); + return this; + } + + public SAXDecoder build() { + return new SAXDecoder(handlerProviders); + } + } -public class SAXDecoder implements Decoder.TextStream { /* Implementations are not intended to be shared across requests. */ public interface ContentHandlerWithResult extends ContentHandler { /* @@ -39,27 +80,17 @@ public interface ContentHandlerWithResult extends ContentHandler { T result(); } - private final Provider> handlers; + private final Map>> handlerProviders; - /** - * You must subclass this, in order to prevent type erasure on {@code T}. In - * addition to making a concrete type, you can also use the following form. - *

- *
- *

- *

-   * new SaxDecoder<Foo>(fooHandlers) {
-   * }; // note the curly braces ensures no type erasure!
-   * 
- */ - protected SAXDecoder(Provider> handlers) { - this.handlers = checkNotNull(handlers, "handlers"); + private SAXDecoder(Map>> handlerProviders) { + this.handlerProviders = handlerProviders; } @Override - public T decode(Reader reader, Type type) throws IOException, DecodeException { - ContentHandlerWithResult handler = handlers.get(); - checkState(handler != null, "%s returned null for type %s", this, type); + public Object decode(Reader reader, Type type) throws IOException, DecodeException { + Provider> handlerProvider = handlerProviders.get(type); + checkState(handlerProvider != null, "type %s not in configured handlers %s", type, handlerProviders.keySet()); + ContentHandlerWithResult handler = handlerProvider.get(); try { XMLReader xmlReader = XMLReaderFactory.createXMLReader(); xmlReader.setFeature("http://xml.org/sax/features/namespaces", false); diff --git a/core/src/test/java/feign/codec/SAXDecoderTest.java b/core/src/test/java/feign/codec/SAXDecoderTest.java new file mode 100644 index 0000000000..01f0d75da1 --- /dev/null +++ b/core/src/test/java/feign/codec/SAXDecoderTest.java @@ -0,0 +1,136 @@ +/* + * 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.codec; + +import dagger.ObjectGraph; +import dagger.Provides; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import org.xml.sax.helpers.DefaultHandler; + +import javax.inject.Inject; +import javax.inject.Provider; +import java.io.IOException; +import java.io.StringReader; +import java.text.ParseException; +import java.util.Set; + +import static dagger.Provides.Type.SET; +import static org.testng.Assert.assertEquals; + +// unbound wildcards are not currently injectable in dagger. +@SuppressWarnings("rawtypes") +public class SAXDecoderTest { + + @dagger.Module(injects = SAXDecoderTest.class) + static class Module { + @Provides(type = SET) Decoder saxDecoder(Provider networkStatus, // + Provider networkStatusAsString) { + return SAXDecoder.builder() // + .addContentHandler(networkStatus) // + .addContentHandler(networkStatusAsString) // + .build(); + } + } + + @Inject Set decoders; + + @BeforeClass void inject() { + ObjectGraph.create(new Module()).inject(this); + } + + @Test public void parsesConfiguredTypes() throws ParseException, IOException { + Decoder decoder = decoders.iterator().next(); + assertEquals(decoder.decode(new StringReader(statusFailed), NetworkStatus.class), NetworkStatus.FAILED); + assertEquals(decoder.decode(new StringReader(statusFailed), String.class), "Failed"); + } + + @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = + "type int not in configured handlers \\[class .*NetworkStatus, class java.lang.String\\]") + public void niceErrorOnUnconfiguredType() throws ParseException, IOException { + Decoder decoder = decoders.iterator().next(); + decoder.decode(new StringReader(statusFailed), int.class); + } + + static String statusFailed = ""// + + "\n"// + + " \n"// + + " \n"// + + " Failed\n"// + + " \n"// + + " \n"// + + ""; + + static enum NetworkStatus { + GOOD, FAILED; + } + + static class NetworkStatusStringHandler extends DefaultHandler implements + SAXDecoder.ContentHandlerWithResult { + @Inject NetworkStatusStringHandler() { + } + + private StringBuilder currentText = new StringBuilder(); + + private String status; + + @Override + public String result() { + return status; + } + + @Override + public void endElement(String uri, String name, String qName) { + if (qName.equals("NeustarNetworkStatus")) { + this.status = currentText.toString().trim(); + } + currentText = new StringBuilder(); + } + + @Override + public void characters(char ch[], int start, int length) { + currentText.append(ch, start, length); + } + } + + static class NetworkStatusHandler extends DefaultHandler implements + SAXDecoder.ContentHandlerWithResult { + @Inject NetworkStatusHandler() { + } + + private StringBuilder currentText = new StringBuilder(); + + private NetworkStatus status; + + @Override + public NetworkStatus result() { + return status; + } + + @Override + public void endElement(String uri, String name, String qName) { + if (qName.equals("NeustarNetworkStatus")) { + this.status = NetworkStatus.valueOf(currentText.toString().trim().toUpperCase()); + } + currentText = new StringBuilder(); + } + + @Override + public void characters(char ch[], int start, int length) { + currentText.append(ch, start, length); + } + } +} diff --git a/core/src/test/java/feign/examples/IAMExample.java b/core/src/test/java/feign/examples/IAMExample.java index f16bb3c274..0a7c63faf3 100644 --- a/core/src/test/java/feign/examples/IAMExample.java +++ b/core/src/test/java/feign/examples/IAMExample.java @@ -23,20 +23,28 @@ import feign.RequestTemplate; import feign.Target; import feign.codec.Decoder; -import feign.codec.Decoders; +import feign.codec.Decoders.ApplyFirstGroup; +import feign.codec.Decoders.TransformFirstGroup; +import feign.codec.SAXDecoder; +import org.xml.sax.helpers.DefaultHandler; + +import javax.inject.Inject; +import javax.inject.Provider; import static dagger.Provides.Type.SET; public class IAMExample { interface IAM { - @RequestLine("GET /?Action=GetUser&Version=2010-05-08") String arn(); + @RequestLine("GET /?Action=GetUser&Version=2010-05-08") Long userId(); } public static void main(String... args) { - IAM iam = Feign.create(new IAMTarget(args[0], args[1]), new IAMModule()); - System.out.println(iam.arn()); + for (Object decodingApproach : new Object[]{new DecodeWithSax(), new DecodeWithRegEx()}) { + IAM iam = Feign.create(new IAMTarget(args[0], args[1]), decodingApproach); + System.out.println(iam.userId()); + } } static class IAMTarget extends AWSSignatureVersion4 implements Target { @@ -64,9 +72,52 @@ private IAMTarget(String accessKey, String secretKey) { } @Module(library = true) - static class IAMModule { - @Provides(type = SET) Decoder decoder() { - return Decoders.firstGroup("([\\S&&[^<]]+)"); + static class DecodeWithRegEx { + @Provides(type = SET) Decoder regExDecoder() { + return new TransformFirstGroup("([0-9]+)", new ApplyFirstGroup() { + + @Override public Long apply(String firstGroup) { + return Long.parseLong(firstGroup); + } + }) { + }; + } + } + + @Module(library = true) + static class DecodeWithSax { + @Provides(type = SET) Decoder saxDecoder(Provider userIdHandler) { + return SAXDecoder.builder() // + .addContentHandler(userIdHandler) // + .build(); + } + } + + static class UserIdHandler extends DefaultHandler implements + SAXDecoder.ContentHandlerWithResult { + @Inject UserIdHandler() { + } + + private StringBuilder currentText = new StringBuilder(); + + private Long userId; + + @Override + public Long result() { + return userId; + } + + @Override + public void endElement(String uri, String name, String qName) { + if (qName.equals("UserId")) { + this.userId = Long.parseLong(currentText.toString().trim()); + } + currentText = new StringBuilder(); + } + + @Override + public void characters(char ch[], int start, int length) { + currentText.append(ch, start, length); } } } From 4b4325c5cf8dce46840c4adee1d17556b576e1f0 Mon Sep 17 00:00:00 2001 From: adriancole Date: Wed, 11 Sep 2013 22:23:14 +0200 Subject: [PATCH 099/179] Remove pattern decoders in favor of SaxDecoder. --- CHANGES.md | 1 + README.md | 13 -- core/src/main/java/feign/codec/Decoders.java | 196 ------------------ core/src/test/java/feign/UtilTest.java | 7 - .../test/java/feign/examples/IAMExample.java | 22 +- 5 files changed, 3 insertions(+), 236 deletions(-) delete mode 100644 core/src/main/java/feign/codec/Decoders.java diff --git a/CHANGES.md b/CHANGES.md index 7743e87859..c7441d884b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,7 @@ ### Version 5.0 * Remove support for Observable methods. * SaxDecoder now decodes multiple types. +* Remove pattern decoders in favor of SaxDecoder. ### Version 4.4.1 * Fix NullPointerException on calling equals and hashCode. diff --git a/README.md b/README.md index 822f0d9ec4..355b894336 100644 --- a/README.md +++ b/README.md @@ -158,16 +158,3 @@ class Overrides { } GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonGitHubModule(), new Overrides()); ``` -#### Pattern Decoders -If you have to only grab a single field from a server response, you may find regular expressions less maintenance than writing a type adapter. - -Here's how our IAM example grabs only one xml element from a response. -```java -@Module(library = true) -static class IAMModule { - @Provides(type = SET) Decoder arnDecoder() { - return Decoders.firstGroup("([\\S&&[^<]]+)"); - } -} -``` - diff --git a/core/src/main/java/feign/codec/Decoders.java b/core/src/main/java/feign/codec/Decoders.java deleted file mode 100644 index 80b61ce999..0000000000 --- a/core/src/main/java/feign/codec/Decoders.java +++ /dev/null @@ -1,196 +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.codec; - -import java.io.IOException; -import java.io.Reader; -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import static feign.Util.checkNotNull; -import static java.lang.String.format; -import static java.util.regex.Pattern.DOTALL; -import static java.util.regex.Pattern.compile; - -/** - * Static utility methods pertaining to {@code Decoder} instances.
- *
- *
- * Pattern Decoders
- *
- * Pattern decoders typically require less initialization, dependencies, and - * code than reflective decoders, but not can be awkward to those unfamiliar - * with regex. Typical use of pattern decoders is to grab a single field from an - * xml response, or parse a list of Strings. The pattern decoders here - * facilitate these use cases. - */ -public class Decoders { - /** - * guava users will implement this with {@code ApplyFirstGroup}. - * - * @param intended result type - */ - public interface ApplyFirstGroup { - /** - * create a new instance from the non-null {@code firstGroup} specified. - */ - T apply(String firstGroup); - } - - /** - * shortcut for
new TransformFirstGroup(pattern, applyFirstGroup){}
when - * {@code String} is the type you are decoding into.
- *
- * Ex. to pull the first interesting element from an xml response:
- *

- *

-   * decodeFirstDirPoolID = firstGroup("<DirPoolID[ˆ>]*>([ˆ<]+)</DirPoolID>");
-   * 
- */ - public static Decoder.TextStream firstGroup(String pattern) { - return new TransformFirstGroup(pattern, IDENTITY) { - }; - } - - /** - * shortcut for
new TransformEachFirstGroup(pattern, applyFirstGroup){}
when - * {@code List} is the type you are decoding into.
- * Ex. to pull a list zones names, which are http paths starting with - * {@code /Rest/Zone/}:
- *

- *

-   * decodeListOfZonesNames = eachFirstGroup("/REST/Zone/([ˆ/]+)/");
-   * 
- */ - public static Decoder.TextStream> eachFirstGroup(String pattern) { - return new TransformEachFirstGroup(pattern, IDENTITY) { - }; - } - - private static String toString(Reader reader) throws IOException { - return TO_STRING.decode(reader, null).toString(); - } - - private static final StringDecoder TO_STRING = new StringDecoder(); - - private static final ApplyFirstGroup IDENTITY = new ApplyFirstGroup() { - @Override - public String apply(String firstGroup) { - return firstGroup; - } - }; - - /** - * The first match group is applied to {@code applyGroups} and result - * returned. If no matches are found, the response is null;
- * Ex. to pull the first interesting element from an xml response:
- *

- *

-   * decodeFirstDirPoolID = new TransformFirstGroup<Long>("<DirPoolID[ˆ>]*>([ˆ<]+)</DirPoolID>", ToLong.INSTANCE) {
-   * };
-   * 
- */ - public static class TransformFirstGroup implements Decoder.TextStream { - private final Pattern patternForMatcher; - private final ApplyFirstGroup applyFirstGroup; - - /** - * You must subclass this, in order to prevent type erasure on {@code T} - * . In addition to making a concrete type, you can also use the - * following form. - *

- *
- *

- *

-     * new TransformFirstGroup<Foo>(pattern, applyFirstGroup) {
-     * }; // note the curly braces ensures no type erasure!
-     * 
- */ - protected TransformFirstGroup(String pattern, ApplyFirstGroup applyFirstGroup) { - this.patternForMatcher = compile(checkNotNull(pattern, "pattern"), DOTALL); - this.applyFirstGroup = checkNotNull(applyFirstGroup, "applyFirstGroup"); - } - - @Override - public T decode(Reader reader, Type type) throws IOException { - Matcher matcher = patternForMatcher.matcher(Decoders.toString(reader)); - if (matcher.find()) { - return applyFirstGroup.apply(matcher.group(1)); - } - return null; - } - - @Override - public String toString() { - return format("decode groups from %s into %s", patternForMatcher, applyFirstGroup); - } - } - - /** - * On the each find the first match group is applied to - * {@code applyFirstGroup} and added to the list returned. If no matches are - * found, the response is an empty list;
- * Ex. to pull a list zones constructed from http paths starting with - * {@code /Rest/Zone/}: - *

- *
- *

- *

-   * decodeListOfZones = new TransformEachFirstGroup("/REST/Zone/([ˆ/]+)/", ToZone.INSTANCE) {
-   * };
-   * 
- */ - public static class TransformEachFirstGroup implements Decoder.TextStream> { - private final Pattern patternForMatcher; - private final ApplyFirstGroup applyFirstGroup; - - /** - * You must subclass this, in order to prevent type erasure on {@code T} - * . In addition to making a concrete type, you can also use the - * following form. - *

- *
- *

- *

-     * new TransformEachFirstGroup<Foo>(pattern, applyFirstGroup) {
-     * }; // note the curly braces ensures no type erasure!
-     * 
- */ - protected TransformEachFirstGroup(String pattern, ApplyFirstGroup applyFirstGroup) { - this.patternForMatcher = compile(checkNotNull(pattern, "pattern"), DOTALL); - this.applyFirstGroup = checkNotNull(applyFirstGroup, "applyFirstGroup"); - } - - @Override - public List decode(Reader reader, Type type) throws IOException { - Matcher matcher = patternForMatcher.matcher(Decoders.toString(reader)); - List result = new ArrayList(); - while (matcher.find()) { - result.add(applyFirstGroup.apply(matcher.group(1))); - } - return result; - } - - @Override - public String toString() { - return format("decode %s into list elements, where each group(1) is transformed with %s", - patternForMatcher, applyFirstGroup); - } - } -} diff --git a/core/src/test/java/feign/UtilTest.java b/core/src/test/java/feign/UtilTest.java index 63a2e9ae22..873e03d3bc 100644 --- a/core/src/test/java/feign/UtilTest.java +++ b/core/src/test/java/feign/UtilTest.java @@ -16,7 +16,6 @@ package feign; import feign.codec.Decoder; -import feign.codec.Decoders; import feign.codec.StringDecoder; import org.testng.annotations.Test; @@ -54,12 +53,6 @@ interface ParameterizedDecoder> extends Decoder.TextStrea assertEquals(last, String.class); } - @Test public void lastTypeFromStaticMethod() throws Exception { - Decoder.TextStream decoder = Decoders.firstGroup("foo"); - Type last = resolveLastTypeParameter(decoder.getClass(), Decoder.class); - assertEquals(last, String.class); - } - @Test public void lastTypeFromAnonymous() throws Exception { Decoder.TextStream decoder = new Decoder.TextStream() { @Override public Reader decode(Reader reader, Type type) { diff --git a/core/src/test/java/feign/examples/IAMExample.java b/core/src/test/java/feign/examples/IAMExample.java index 0a7c63faf3..540ca0faf7 100644 --- a/core/src/test/java/feign/examples/IAMExample.java +++ b/core/src/test/java/feign/examples/IAMExample.java @@ -23,8 +23,6 @@ import feign.RequestTemplate; import feign.Target; import feign.codec.Decoder; -import feign.codec.Decoders.ApplyFirstGroup; -import feign.codec.Decoders.TransformFirstGroup; import feign.codec.SAXDecoder; import org.xml.sax.helpers.DefaultHandler; @@ -40,11 +38,8 @@ interface IAM { } public static void main(String... args) { - - for (Object decodingApproach : new Object[]{new DecodeWithSax(), new DecodeWithRegEx()}) { - IAM iam = Feign.create(new IAMTarget(args[0], args[1]), decodingApproach); - System.out.println(iam.userId()); - } + IAM iam = Feign.create(new IAMTarget(args[0], args[1]), new DecodeWithSax()); + System.out.println(iam.userId()); } static class IAMTarget extends AWSSignatureVersion4 implements Target { @@ -71,19 +66,6 @@ private IAMTarget(String accessKey, String secretKey) { } } - @Module(library = true) - static class DecodeWithRegEx { - @Provides(type = SET) Decoder regExDecoder() { - return new TransformFirstGroup("([0-9]+)", new ApplyFirstGroup() { - - @Override public Long apply(String firstGroup) { - return Long.parseLong(firstGroup); - } - }) { - }; - } - } - @Module(library = true) static class DecodeWithSax { @Provides(type = SET) Decoder saxDecoder(Provider userIdHandler) { From cd5405eb5994f361c1d53357c739008c43f18b9d Mon Sep 17 00:00:00 2001 From: "David M. Carr" Date: Thu, 12 Sep 2013 23:54:24 -0400 Subject: [PATCH 100/179] Simplify Encoder/Decoder interfaces (#53) This is intended as a step towards simplifying Feign. This changeset removes the generics from both interfaces, and changes their Dagger bindings from SET to UNIQUE. Additionally, in changing the signatures for Encoder/Decoder, it focuses on use of the RequestTemplate and Response objects, allowing us to extend them in the future to support binary data without needing to change the Encoder/Decoder signatures again. --- CHANGES.md | 2 + README.md | 40 ++++---- core/src/main/java/feign/Feign.java | 12 +++ core/src/main/java/feign/FeignException.java | 4 +- core/src/main/java/feign/Logger.java | 2 +- core/src/main/java/feign/MethodHandler.java | 13 +-- core/src/main/java/feign/ReflectiveFeign.java | 86 +++-------------- core/src/main/java/feign/Util.java | 7 +- core/src/main/java/feign/codec/Decoder.java | 93 ++++++++++--------- core/src/main/java/feign/codec/Encoder.java | 79 +++++++++------- .../src/main/java/feign/codec/SAXDecoder.java | 18 +++- .../main/java/feign/codec/StringDecoder.java | 31 +++++-- core/src/test/java/feign/FeignTest.java | 41 ++++---- core/src/test/java/feign/LoggerTest.java | 9 +- core/src/test/java/feign/UtilTest.java | 33 +++---- .../java/feign/codec/DefaultDecoderTest.java | 74 +++++++++++++++ .../java/feign/codec/DefaultEncoderTest.java | 39 ++++++++ .../test/java/feign/codec/SAXDecoderTest.java | 24 ++--- .../java/feign/examples/GitHubExample.java | 19 +++- gson/src/main/java/feign/gson/GsonModule.java | 27 ++++-- .../test/java/feign/gson/GsonModuleTest.java | 56 +++++------ 21 files changed, 416 insertions(+), 293 deletions(-) create mode 100644 core/src/test/java/feign/codec/DefaultDecoderTest.java create mode 100644 core/src/test/java/feign/codec/DefaultEncoderTest.java diff --git a/CHANGES.md b/CHANGES.md index c7441d884b..fc3771644b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,8 @@ * Remove support for Observable methods. * SaxDecoder now decodes multiple types. * Remove pattern decoders in favor of SaxDecoder. +* Use single non-generic Decoder/Encoder instead of sets of type-specific Decoders/Encoders. +* Decoders/Encoders are now more flexible, having access to the Response/RequestTemplate respectively. ### Version 4.4.1 * Fix NullPointerException on calling equals and hashCode. diff --git a/README.md b/README.md index 355b894336..7a67d04e96 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ You can find [several examples](https://github.com/Netflix/feign/tree/master/fei ### Integrations Feign intends to work well within Netflix and other Open Source communities. Modules are welcome to integrate with your favorite projects! ### Gson -[GsonModule](https://github.com/Netflix/feign/tree/master/feign-gson) adds default encoders and decoders so you get get started with a json api. +[GsonModule](https://github.com/Netflix/feign/tree/master/feign-gson) adds default encoders and decoders so you get get started with a JSON api. Integration requires you pass `new GsonModule()` to `Feign.create()`, or add it to your graph with Dagger: ```java @@ -101,46 +101,46 @@ MyService api = Feign.create(MyService.class, "https://myAppProd", new RibbonMod ### Decoders The last argument to `Feign.create` allows you to specify additional configuration such as how to decode a responses, modeled in Dagger. -If any methods in your interface return types besides `void` or `String`, you'll need to configure a `Decoder.TextStream` or a general one for all types (`Decoder.TextStream`). +If any methods in your interface return types besides `Response`, `void` or `String`, you'll need to configure a `Decoder`. -The `GsonModule` in the `feign-gson` extension configures a (`Decoder.TextStream`) which parses objects from json using reflection. +The `GsonModule` in the `feign-gson` extension configures a `Decoder` which parses objects from JSON using reflection. Here's how you could write this yourself, using whatever library you prefer: ```java @Module(library = true) static class JsonModule { - @Provides(type = SET) Decoder decoder(final JsonParser parser) { - return new Decoder.TextStream() { + @Provides Decoder decoder(final JsonParser parser) { + return new Decoder() { - @Override public Object decode(Reader reader, Type type) throws IOException { - return parser.readJson(reader, type); + @Override public Object decode(Response response, Type type) throws IOException { + return parser.readJson(response.body().asReader(), type); } }; } } ``` -#### Type-specific Decoders -The generic parameter of `Decoder.TextStream` designates which The type parameter is either a concrete type, or `Object`, if your decoder can handle multiple types. To add a type-specific decoder, ensure your type parameter is correct. Here's an example of an xml decoder that will only apply to methods that return `ZoneList`. - -``` -@Provides(type = SET) Decoder zoneListDecoder(Provider handlers) { - return new SAXDecoder(handlers){}; -} -``` ### Advanced usage and Dagger #### Dagger Feign can be directly wired into Dagger which keeps things at compile time and Android friendly. As opposed to exposing builders for config, Feign intends users to embed their config in Dagger. -Where possible, Feign configuration uses normal Dagger conventions. For example, `Decoder` bindings are of `Provider.Type.SET`, meaning you can make multiple bindings for all the different types you return. Here's an example of multiple decoder bindings. +Where possible, Feign configuration uses normal Dagger conventions. For example, `RequestInterceptor` bindings are of `Provider.Type.SET`, meaning you can have multiple interceptors. Here's an example of multiple interceptor bindings. ```java -@Provides(type = SET) Decoder recordListDecoder(Provider handlers) { - return new SAXDecoder>(handlers){}; +@Provides(type = SET) RequestInterceptor forwardedForInterceptor() { + return new RequestInterceptor() { + @Override public void apply(RequestTemplate template) { + template.header("X-Forwarded-For", "origin.host.com"); + } + }; } -@Provides(type = SET) Decoder directionalRecordListDecoder(Provider handlers) { - return new SAXDecoder>(handlers){}; +@Provides(type = SET) RequestInterceptor userAgentInterceptor() { + return new RequestInterceptor() { + @Override public void apply(RequestTemplate template) { + template.header("User-Agent", "My Cool Client"); + } + }; } ``` #### Logging diff --git a/core/src/main/java/feign/Feign.java b/core/src/main/java/feign/Feign.java index f4e8c1f48d..6bbf4715a3 100644 --- a/core/src/main/java/feign/Feign.java +++ b/core/src/main/java/feign/Feign.java @@ -21,6 +21,8 @@ import feign.Logger.NoOpLogger; import feign.Request.Options; import feign.Target.HardCodedTarget; +import feign.codec.Decoder; +import feign.codec.Encoder; import feign.codec.ErrorDecoder; import javax.net.ssl.HostnameVerifier; @@ -107,6 +109,16 @@ public static class Defaults { return new NoOpLogger(); } + @Provides + Encoder defaultEncoder() { + return new Encoder.Default(); + } + + @Provides + Decoder defaultDecoder() { + return new Decoder.Default(); + } + @Provides ErrorDecoder errorDecoder() { return new ErrorDecoder.Default(); } diff --git a/core/src/main/java/feign/FeignException.java b/core/src/main/java/feign/FeignException.java index ebdf7b0650..c158aeb2e5 100644 --- a/core/src/main/java/feign/FeignException.java +++ b/core/src/main/java/feign/FeignException.java @@ -35,8 +35,8 @@ public static FeignException errorStatus(String methodKey, Response response) { String message = format("status %s reading %s", response.status(), methodKey); try { if (response.body() != null) { - String body = toString.decode(response.body().asReader(), String.class); - response = Response.create(response.status(), response.reason(), response.headers(), body.toString()); + String body = toString.decode(response, String.class).toString(); + response = Response.create(response.status(), response.reason(), response.headers(), body); message += "; content:\n" + body; } } catch (IOException ignored) { // NOPMD diff --git a/core/src/main/java/feign/Logger.java b/core/src/main/java/feign/Logger.java index 48853b1f29..cd99901c86 100644 --- a/core/src/main/java/feign/Logger.java +++ b/core/src/main/java/feign/Logger.java @@ -191,7 +191,7 @@ Response logAndRebufferResponse(String configKey, Level logLevel, Response respo 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()); + ensureClosed(body); } } } diff --git a/core/src/main/java/feign/MethodHandler.java b/core/src/main/java/feign/MethodHandler.java index 767f1e4bfa..7b21938545 100644 --- a/core/src/main/java/feign/MethodHandler.java +++ b/core/src/main/java/feign/MethodHandler.java @@ -52,7 +52,7 @@ static class Factory { } public MethodHandler create(Target target, MethodMetadata md, BuildTemplateFromArgs buildTemplateFromArgs, - Options options, Decoder.TextStream decoder, ErrorDecoder errorDecoder) { + Options options, Decoder decoder, ErrorDecoder errorDecoder) { return new SynchronousMethodHandler(target, client, retryer, requestInterceptors, logger, logLevel, md, buildTemplateFromArgs, options, decoder, errorDecoder); } @@ -82,14 +82,14 @@ static final class SynchronousMethodHandler implements MethodHandler { private final Provider logLevel; private final BuildTemplateFromArgs buildTemplateFromArgs; private final Options options; - private final Decoder.TextStream decoder; + private final Decoder decoder; private final ErrorDecoder errorDecoder; private SynchronousMethodHandler(Target target, Client client, Provider retryer, Set requestInterceptors, Logger logger, Provider logLevel, MethodMetadata metadata, BuildTemplateFromArgs buildTemplateFromArgs, Options options, - Decoder.TextStream decoder, ErrorDecoder errorDecoder) { + Decoder decoder, ErrorDecoder errorDecoder) { this.target = checkNotNull(target, "target"); this.client = checkNotNull(client, "client for %s", target); this.retryer = checkNotNull(retryer, "retryer for %s", target); @@ -169,13 +169,8 @@ Request targetRequest(RequestTemplate template) { } Object decode(Response response) throws Throwable { - if (metadata.returnType().equals(Response.class)) { - return response; - } else if (metadata.returnType() == void.class || response.body() == null) { - return null; - } try { - return decoder.decode(response.body().asReader(), metadata.returnType()); + return decoder.decode(response, metadata.returnType()); } catch (FeignException e) { throw e; } catch (RuntimeException e) { diff --git a/core/src/main/java/feign/ReflectiveFeign.java b/core/src/main/java/feign/ReflectiveFeign.java index 81029285d7..b1d60690f4 100644 --- a/core/src/main/java/feign/ReflectiveFeign.java +++ b/core/src/main/java/feign/ReflectiveFeign.java @@ -21,16 +21,13 @@ import feign.codec.EncodeException; import feign.codec.Encoder; import feign.codec.ErrorDecoder; -import feign.codec.StringDecoder; import javax.inject.Inject; import java.lang.reflect.InvocationHandler; 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; import java.util.Map; @@ -39,9 +36,6 @@ import static feign.Util.checkArgument; import static feign.Util.checkNotNull; -import static feign.Util.checkState; -import static feign.Util.resolveLastTypeParameter; -import static java.lang.String.format; @SuppressWarnings("rawtypes") public class ReflectiveFeign extends Feign { @@ -122,14 +116,6 @@ public static class Module { return Collections.emptySet(); } - @Provides(type = Provides.Type.SET_VALUES) Set noEncoders() { - return Collections.emptySet(); - } - - @Provides(type = Provides.Type.SET_VALUES) Set noDecoders() { - return Collections.emptySet(); - } - @Provides Feign provideFeign(ReflectiveFeign in) { return in; } @@ -138,46 +124,20 @@ public static class Module { static final class ParseHandlersByName { private final Contract contract; private final Options options; - private final Map> encoders = new HashMap>(); - private final Encoder.Text> formEncoder; - private final Map> decoders = new HashMap>(); + private final Encoder encoder; + private final Decoder decoder; private final ErrorDecoder errorDecoder; private final MethodHandler.Factory factory; @SuppressWarnings("unchecked") - @Inject ParseHandlersByName(Contract contract, Options options, Set encoders, Set decoders, + @Inject ParseHandlersByName(Contract contract, Options options, Encoder encoder, Decoder decoder, ErrorDecoder errorDecoder, MethodHandler.Factory factory) { this.contract = contract; this.options = options; this.factory = factory; this.errorDecoder = errorDecoder; - for (Encoder encoder : encoders) { - checkState(encoder instanceof Encoder.Text, - "Currently, only Encoder.Text is supported. Found: ", encoder); - Type type = resolveLastTypeParameter(encoder.getClass(), Encoder.class); - this.encoders.put(type, Encoder.Text.class.cast(encoder)); - } - try { - Type formEncoderType = getClass().getDeclaredField("formEncoder").getGenericType(); - Type formType = resolveLastTypeParameter(formEncoderType, Encoder.class); - Encoder.Text formEncoder = this.encoders.get(formType); - if (formEncoder == null) { - formEncoder = this.encoders.get(Object.class); - } - this.formEncoder = (Encoder.Text) formEncoder; - } catch (NoSuchFieldException e) { - throw new AssertionError(e); - } - StringDecoder stringDecoder = new StringDecoder(); - this.decoders.put(void.class, stringDecoder); - this.decoders.put(Response.class, stringDecoder); - this.decoders.put(String.class, stringDecoder); - for (Decoder decoder : decoders) { - checkState(decoder instanceof Decoder.TextStream, - "Currently, only Decoder.TextStream is supported. Found: ", decoder); - Type type = resolveLastTypeParameter(decoder.getClass(), Decoder.class); - this.decoders.put(type, Decoder.TextStream.class.cast(decoder)); - } + this.encoder = checkNotNull(encoder, "encoder"); + this.decoder = checkNotNull(decoder, "decoder"); } public Map apply(Target key) { @@ -186,32 +146,12 @@ public Map apply(Target key) { for (MethodMetadata md : metadata) { BuildTemplateByResolvingArgs buildTemplate; if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) { - if (formEncoder == null) { - throw new IllegalStateException(format("%s needs @Provides(type = Set) Encoder encoder()" + - "{ // Encoder.Text> or Encoder.Text}", md.configKey())); - } - buildTemplate = new BuildFormEncodedTemplateFromArgs(md, formEncoder); + buildTemplate = new BuildFormEncodedTemplateFromArgs(md, encoder); } else if (md.bodyIndex() != null) { - Encoder.Text encoder = encoders.get(md.bodyType()); - if (encoder == null) { - encoder = encoders.get(Object.class); - } - if (encoder == null) { - throw new IllegalStateException(format("%s needs @Provides(type = Set) Encoder encoder()" + - "{ // Encoder.Text<%s> or Encoder.Text}", md.configKey(), md.bodyType())); - } buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder); } else { buildTemplate = new BuildTemplateByResolvingArgs(md); } - Decoder.TextStream decoder = decoders.get(md.returnType()); - if (decoder == null) { - decoder = decoders.get(Object.class); - } - if (decoder == null) { - throw new IllegalStateException(format("%s needs @Provides(type = Set) Decoder decoder()" + - "{ // Decoder.TextStream<%s> or Decoder.TextStream}", md.configKey(), md.returnType())); - } result.put(md.configKey(), factory.create(key, md, buildTemplate, options, decoder, errorDecoder)); } return result; @@ -249,11 +189,11 @@ protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map> formEncoder; + private final Encoder encoder; - private BuildFormEncodedTemplateFromArgs(MethodMetadata metadata, Encoder.Text> formEncoder) { + private BuildFormEncodedTemplateFromArgs(MethodMetadata metadata, Encoder encoder) { super(metadata); - this.formEncoder = formEncoder; + this.encoder = encoder; } @Override @@ -264,7 +204,7 @@ protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map encoder; + private final Encoder encoder; - private BuildEncodedTemplateFromArgs(MethodMetadata metadata, Encoder.Text encoder) { + private BuildEncodedTemplateFromArgs(MethodMetadata metadata, Encoder encoder) { super(metadata); this.encoder = encoder; } @@ -287,7 +227,7 @@ protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map Collection valuesOrEmpty(Map> map, St return map.containsKey(key) ? map.get(key) : Collections.emptyList(); } - public static void ensureClosed(Response.Body body) { - if (body != null) { + public static void ensureClosed(Closeable closeable) { + if (closeable != null) { try { - body.close(); + closeable.close(); } catch (IOException ignored) { // NOPMD } } diff --git a/core/src/main/java/feign/codec/Decoder.java b/core/src/main/java/feign/codec/Decoder.java index 8492d143b4..1a7865cab5 100644 --- a/core/src/main/java/feign/codec/Decoder.java +++ b/core/src/main/java/feign/codec/Decoder.java @@ -19,64 +19,73 @@ import feign.Response; import java.io.IOException; -import java.io.Reader; import java.lang.reflect.Type; +import static java.lang.String.format; + /** - * Decodes an HTTP response into a given type. Invoked when + * Decodes an HTTP response into a single object of the given {@code Type}. Invoked when * {@link Response#status()} is in the 2xx range. Like * {@code javax.websocket.Decoder}, except that the decode method is passed the - * generic type of the target.
+ * generic type of the target. + * + *

+ * Example Implementation:
+ *

+ *

+ * public class GsonDecoder implements Decoder {
+ *   private final Gson gson;
+ *
+ *   public GsonDecoder(Gson gson) {
+ *     this.gson = gson;
+ *   }
  *
- * @param  input that can be derived from {@link feign.Response.Body}.
- * @param  widest type an instance of this can decode.
+ *   @Override
+ *   public Object decode(Response response, Type type) throws IOException {
+ *     try {
+ *       return gson.fromJson(response.body().asReader(), type);
+ *     } catch (JsonIOException e) {
+ *       if (e.getCause() != null &&
+ *           e.getCause() instanceof IOException) {
+ *         throw IOException.class.cast(e.getCause());
+ *       }
+ *       throw e;
+ *     }
+ *   }
+ * }
+ * 
*/ -public interface Decoder { +public interface Decoder { /** - * Implement this to decode a resource to an object into a single object. + * Decodes a response into a single object. * If you need to wrap exceptions, please do so via {@link DecodeException}. * - * @param input if {@code Closeable}, no need to close this, as the caller - * manages resources. + * @param response the response to decode * @param type Target object type. * @return instance of {@code type} * @throws IOException will be propagated safely to the caller. - * @throws DecodeException when decoding failed due to a checked exception - * besides IOException. - * @throws FeignException when decoding succeeds, but conveys the operation - * failed. + * @throws DecodeException when decoding failed due to a checked exception besides IOException. + * @throws FeignException when decoding succeeds, but conveys the operation failed. */ - T decode(I input, Type type) throws IOException, DecodeException, FeignException; + Object decode(Response response, Type type) throws IOException, DecodeException, FeignException; /** - * Used for text-based apis, follows - * {@link Decoder#decode(Object, java.lang.reflect.Type)} - * semantics, applied to inputs of type {@link java.io.Reader}.
- * Ex.
- *

- *

-   * public class GsonDecoder implements Decoder.TextStream<Object> {
-   *   private final Gson gson;
-   *
-   *   public GsonDecoder(Gson gson) {
-   *     this.gson = gson;
-   *   }
-   *
-   *   @Override
-   *   public Object decode(Reader reader, Type type) throws IOException {
-   *     try {
-   *       return gson.fromJson(reader, type);
-   *     } catch (JsonIOException e) {
-   *       if (e.getCause() != null &&
-   *           e.getCause() instanceof IOException) {
-   *         throw IOException.class.cast(e.getCause());
-   *       }
-   *       throw e;
-   *     }
-   *   }
-   * }
-   * 
+ * Default implementation of {@code Decoder} that supports {@code void}, {@code Response}, and {@code String} + * signatures. */ - public interface TextStream extends Decoder { + public class Default implements Decoder { + private final StringDecoder stringDecoder = new StringDecoder(); + + @Override + public Object decode(Response response, Type type) throws IOException { + if (Response.class.equals(type)) { + return response; + } else if (String.class.equals(type)) { + return stringDecoder.decode(response, type); + } else if (void.class.equals(type) || response.body() == null) { + return null; + } + throw new DecodeException(format("%s is not a type supported by this decoder.", type)); + } } } diff --git a/core/src/main/java/feign/codec/Encoder.java b/core/src/main/java/feign/codec/Encoder.java index 80614b53fd..ab7e39f8c7 100644 --- a/core/src/main/java/feign/codec/Encoder.java +++ b/core/src/main/java/feign/codec/Encoder.java @@ -15,61 +15,70 @@ */ package feign.codec; +import feign.RequestTemplate; + +import static java.lang.String.format; + /** - * Encodes an object into an HTTP request body. Like - * {@code javax.websocket.Encoder}.
- * {@code Encoder} is used when a method parameter has no {@code *Param} - * annotation. For example:
+ * Encodes an object into an HTTP request body. Like {@code javax.websocket.Encoder}. + * {@code Encoder} is used when a method parameter has no {@code @Param} annotation. + * For example:
*

*

  * @POST
  * @Path("/")
  * void create(User user);
  * 
+ * Example implementation:
+ *

+ *

+ * public class GsonEncoder implements Encoder {
+ *   private final Gson gson;
+ *
+ *   public GsonEncoder(Gson gson) {
+ *     this.gson = gson;
+ *   }
+ *
+ *   @Override
+ *   public void encode(Object object, RequestTemplate template) {
+ *     template.body(gson.toJson(object));
+ *   }
+ * }
+ * 
+ * *

*

Form encoding

*
* If any parameters are found in {@link feign.MethodMetadata#formParams()}, they will be - * collected and passed to {@code Encoder.Text>}. + * collected and passed to the Encoder as a {@code Map}. *
*
  * @POST
  * @Path("/")
  * Session login(@Named("username") String username, @Named("password") String password);
  * 
- * - * @param widest type an instance of this can encode. */ -public interface Encoder { - +public interface Encoder { /** - * Converts objects to an appropriate text representation.
- * Ex.
- *

- *

-   * public class GsonEncoder implements Encoder.Text<Object> {
-   *     private final Gson gson;
-   *
-   *     public GsonEncoder(Gson gson) {
-   *         this.gson = gson;
-   *     }
+   * Converts objects to an appropriate representation in the template.
    *
-   *     @Override
-   *     public String encode(Object object) {
-   *         return gson.toJson(object);
-   *     }
-   * }
-   * 
+ * @param object what to encode as the request body. + * @param template the request template to populate. + * @throws EncodeException when encoding failed due to a checked exception. + */ + void encode(Object object, RequestTemplate template) throws EncodeException; + + /** + * Default implementation of {@code Encoder} that supports {@code String}s only. */ - interface Text extends Encoder { - /** - * Implement this to encode an object as a String.. If you need to wrap - * exceptions, please do so via {@link EncodeException} - * - * @param object what to encode as the request body. - * @return the encoded object as a string. * @throws EncodeException - * when encoding failed due to a checked exception. - */ - String encode(T object) throws EncodeException; + public class Default implements Encoder { + @Override + public void encode(Object object, RequestTemplate template) throws EncodeException { + if (object instanceof String) { + template.body(object.toString()); + } else if (object != null) { + throw new EncodeException(format("%s is not a type supported by this encoder.", object.getClass())); + } + } } } diff --git a/core/src/main/java/feign/codec/SAXDecoder.java b/core/src/main/java/feign/codec/SAXDecoder.java index 48481adb7e..46cf17508f 100644 --- a/core/src/main/java/feign/codec/SAXDecoder.java +++ b/core/src/main/java/feign/codec/SAXDecoder.java @@ -15,6 +15,7 @@ */ package feign.codec; +import feign.Response; import org.xml.sax.ContentHandler; import org.xml.sax.InputSource; import org.xml.sax.SAXException; @@ -30,6 +31,7 @@ import static feign.Util.checkNotNull; import static feign.Util.checkState; +import static feign.Util.ensureClosed; import static feign.Util.resolveLastTypeParameter; /** @@ -38,7 +40,7 @@ *

* *

- * @Provides(type = SET)
+ * @Provides
  * Decoder saxDecoder(Provider<ContentHandlerForFoo> foo, //
  *         Provider<ContentHandlerForBar> bar) {
  *     return SAXDecoder.builder() //
@@ -48,7 +50,7 @@
  * }
  * 
*/ -public class SAXDecoder implements Decoder.TextStream { +public class SAXDecoder implements Decoder { public static Builder builder() { return new Builder(); @@ -87,7 +89,10 @@ private SAXDecoder(Map>> ha } @Override - public Object decode(Reader reader, Type type) throws IOException, DecodeException { + public Object decode(Response response, Type type) throws IOException, DecodeException { + if (response.body() == null) { + return null; + } Provider> handlerProvider = handlerProviders.get(type); checkState(handlerProvider != null, "type %s not in configured handlers %s", type, handlerProviders.keySet()); ContentHandlerWithResult handler = handlerProvider.get(); @@ -96,7 +101,12 @@ public Object decode(Reader reader, Type type) throws IOException, DecodeExcepti xmlReader.setFeature("http://xml.org/sax/features/namespaces", false); xmlReader.setFeature("http://xml.org/sax/features/validation", false); xmlReader.setContentHandler(handler); - xmlReader.parse(new InputSource(reader)); + Reader reader = response.body().asReader(); + try { + xmlReader.parse(new InputSource(reader)); + } finally { + ensureClosed(reader); + } return handler.result(); } catch (SAXException e) { throw new DecodeException(e.getMessage(), e); diff --git a/core/src/main/java/feign/codec/StringDecoder.java b/core/src/main/java/feign/codec/StringDecoder.java index 8711b2d424..93f66eac82 100644 --- a/core/src/main/java/feign/codec/StringDecoder.java +++ b/core/src/main/java/feign/codec/StringDecoder.java @@ -15,26 +15,39 @@ */ package feign.codec; +import feign.Response; + import java.io.IOException; import java.io.Reader; import java.lang.reflect.Type; import java.nio.CharBuffer; +import static feign.Util.ensureClosed; + /** * Adapted from {@code com.google.common.io.CharStreams.toString()}. */ -public class StringDecoder implements Decoder.TextStream { +public class StringDecoder implements Decoder { private static final int BUF_SIZE = 0x800; // 2K chars (4K bytes) @Override - public String decode(Reader from, Type type) throws IOException { - StringBuilder to = new StringBuilder(); - CharBuffer buf = CharBuffer.allocate(BUF_SIZE); - while (from.read(buf) != -1) { - buf.flip(); - to.append(buf); - buf.clear(); + public Object decode(Response response, Type type) throws IOException { + Response.Body body = response.body(); + if (body == null) { + return null; + } + Reader from = body.asReader(); + try { + StringBuilder to = new StringBuilder(); + CharBuffer buf = CharBuffer.allocate(BUF_SIZE); + while (from.read(buf) != -1) { + buf.flip(); + to.append(buf); + buf.clear(); + } + return to.toString(); + } finally { + ensureClosed(from); } - return to.toString(); } } diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index 2c2b7b90eb..58fdc54b67 100644 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -35,7 +35,6 @@ import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLSocketFactory; import java.io.IOException; -import java.io.Reader; import java.lang.reflect.Type; import java.net.URI; import java.util.Arrays; @@ -74,20 +73,16 @@ void login( @RequestLine("GET /?1={1}&2={2}") Response queryParams(@Named("1") String one, @Named("2") Iterable twos); - @dagger.Module(library = true) + @dagger.Module(overrides = true, library = true) static class Module { - @Provides(type = SET) Encoder defaultEncoder() { - return new Encoder.Text() { - @Override public String encode(Object object) { - return object.toString(); - } - }; - } - - @Provides(type = SET) Encoder formEncoder() { - return new Encoder.Text>() { - @Override public String encode(Map object) { - return Joiner.on(',').withKeyValueSeparator("=").join(object); + @Provides Encoder defaultEncoder() { + return new Encoder() { + @Override public void encode(Object object, RequestTemplate template) { + if (object instanceof Map) { + template.body(Joiner.on(',').withKeyValueSeparator("=").join((Map) object)); + } else { + template.body(object.toString()); + } } }; } @@ -315,10 +310,10 @@ public void canOverrideErrorDecoder() throws IOException, InterruptedException { @dagger.Module(overrides = true, library = true, includes = TestInterface.Module.class) static class DecodeFail { - @Provides(type = SET) Decoder decoder() { - return new Decoder.TextStream() { + @Provides Decoder decoder() { + return new Decoder() { @Override - public String decode(Reader reader, Type type) throws IOException { + public Object decode(Response response, Type type) { return "fail"; } }; @@ -343,11 +338,11 @@ public void overrideTypeSpecificDecoder() throws IOException, InterruptedExcepti @dagger.Module(overrides = true, library = true, includes = TestInterface.Module.class) static class RetryableExceptionOnRetry { - @Provides(type = SET) Decoder decoder() { + @Provides Decoder decoder() { return new StringDecoder() { @Override - public String decode(Reader reader, Type type) throws RetryableException, IOException { - String string = super.decode(reader, type); + public Object decode(Response response, Type type) throws IOException, FeignException { + String string = super.decode(response, type).toString(); if ("retry!".equals(string)) throw new RetryableException(string, null); return string; @@ -378,10 +373,10 @@ public void retryableExceptionInDecoder() throws IOException, InterruptedExcepti @dagger.Module(overrides = true, library = true, includes = TestInterface.Module.class) static class IOEOnDecode { - @Provides(type = SET) Decoder decoder() { - return new Decoder.TextStream() { + @Provides Decoder decoder() { + return new Decoder() { @Override - public String decode(Reader reader, Type type) throws IOException { + public Object decode(Response response, Type type) throws IOException { throw new IOException("error reading response"); } }; diff --git a/core/src/test/java/feign/LoggerTest.java b/core/src/test/java/feign/LoggerTest.java index a7a715ed2b..7412377223 100644 --- a/core/src/test/java/feign/LoggerTest.java +++ b/core/src/test/java/feign/LoggerTest.java @@ -32,7 +32,6 @@ import java.util.List; 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; @@ -132,10 +131,10 @@ public void levelEmits(final Logger.Level logLevel, List expectedMessage this.logLevel = logLevel; } - @Provides(type = SET) Encoder defaultEncoder() { - return new Encoder.Text() { - @Override public String encode(Object object) { - return object.toString(); + @Provides Encoder defaultEncoder() { + return new Encoder() { + @Override public void encode(Object object, RequestTemplate template) { + template.body(object.toString()); } }; } diff --git a/core/src/test/java/feign/UtilTest.java b/core/src/test/java/feign/UtilTest.java index 873e03d3bc..322bffe4cc 100644 --- a/core/src/test/java/feign/UtilTest.java +++ b/core/src/test/java/feign/UtilTest.java @@ -16,7 +16,6 @@ package feign; import feign.codec.Decoder; -import feign.codec.StringDecoder; import org.testng.annotations.Test; import java.io.Reader; @@ -31,42 +30,44 @@ public class UtilTest { interface LastTypeParameter { final List LIST_STRING = null; - final Decoder.TextStream> DECODER_LIST_STRING = null; - final Decoder.TextStream> DECODER_WILDCARD_LIST_STRING = null; + final Parameterized> PARAMETERIZED_LIST_STRING = null; + final Parameterized> PARAMETERIZED_WILDCARD_LIST_STRING = null; final ParameterizedDecoder> PARAMETERIZED_DECODER_LIST_STRING = null; final ParameterizedDecoder PARAMETERIZED_DECODER_UNBOUND = null; } - interface ParameterizedDecoder> extends Decoder.TextStream { + interface ParameterizedDecoder> extends Decoder { + } + + interface Parameterized { + } + + class ParameterizedSubtype implements Parameterized { } @Test public void resolveLastTypeParameterWhenNotSubtype() throws Exception { - Type context = LastTypeParameter.class.getDeclaredField("DECODER_LIST_STRING").getGenericType(); + Type context = LastTypeParameter.class.getDeclaredField("PARAMETERIZED_LIST_STRING").getGenericType(); Type listStringType = LastTypeParameter.class.getDeclaredField("LIST_STRING").getGenericType(); - Type last = resolveLastTypeParameter(context, Decoder.class); + Type last = resolveLastTypeParameter(context, Parameterized.class); assertEquals(last, listStringType); } @Test public void lastTypeFromInstance() throws Exception { - Decoder.TextStream decoder = new StringDecoder(); - Type last = resolveLastTypeParameter(decoder.getClass(), Decoder.class); + Parameterized instance = new ParameterizedSubtype(); + Type last = resolveLastTypeParameter(instance.getClass(), Parameterized.class); assertEquals(last, String.class); } @Test public void lastTypeFromAnonymous() throws Exception { - Decoder.TextStream decoder = new Decoder.TextStream() { - @Override public Reader decode(Reader reader, Type type) { - return null; - } - }; - Type last = resolveLastTypeParameter(decoder.getClass(), Decoder.class); + Parameterized instance = new Parameterized() {}; + Type last = resolveLastTypeParameter(instance.getClass(), Parameterized.class); assertEquals(last, Reader.class); } @Test public void resolveLastTypeParameterWhenWildcard() throws Exception { - Type context = LastTypeParameter.class.getDeclaredField("DECODER_WILDCARD_LIST_STRING").getGenericType(); + Type context = LastTypeParameter.class.getDeclaredField("PARAMETERIZED_WILDCARD_LIST_STRING").getGenericType(); Type listStringType = LastTypeParameter.class.getDeclaredField("LIST_STRING").getGenericType(); - Type last = resolveLastTypeParameter(context, Decoder.class); + Type last = resolveLastTypeParameter(context, Parameterized.class); assertEquals(last, listStringType); } diff --git a/core/src/test/java/feign/codec/DefaultDecoderTest.java b/core/src/test/java/feign/codec/DefaultDecoderTest.java new file mode 100644 index 0000000000..d02f8a240c --- /dev/null +++ b/core/src/test/java/feign/codec/DefaultDecoderTest.java @@ -0,0 +1,74 @@ +/* + * 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.codec; + +import feign.FeignException; +import feign.Response; +import org.testng.annotations.Test; +import org.w3c.dom.Document; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; + +public class DefaultDecoderTest { + private final Decoder decoder = new Decoder.Default(); + + @Test public void testDecodesToVoid() throws Exception { + assertEquals(decoder.decode(knownResponse(), void.class), null); + } + + @Test public void testDecodesToResponse() throws Exception { + Response response = knownResponse(); + Object decodedObject = decoder.decode(response, Response.class); + assertEquals(decodedObject.getClass(), Response.class, ""); + Response decodedResponse = (Response) decodedObject; + assertEquals(decodedResponse.status(), response.status()); + assertEquals(decodedResponse.reason(), response.reason()); + assertEquals(decodedResponse.headers(), response.headers()); + assertEquals(decodedResponse.body().toString(), response.body().toString()); + } + + @Test public void testDecodesToString() throws Exception { + Response response = knownResponse(); + Object decodedObject = decoder.decode(response, String.class); + assertEquals(decodedObject.getClass(), String.class); + assertEquals(decodedObject.toString(), response.body().toString()); + } + + @Test public void testDecodesNullBodyToNull() throws Exception { + assertNull(decoder.decode(nullBodyResponse(), Document.class)); + } + + @Test(expectedExceptions = DecodeException.class, expectedExceptionsMessageRegExp = ".* is not a type supported by this decoder.") + public void testRefusesToDecodeOtherTypes() throws Exception { + decoder.decode(knownResponse(), Document.class); + } + + private Response knownResponse() { + Map> headers = new HashMap>(); + headers.put("Content-Type", Collections.singleton("text/plain")); + return Response.create(200, "OK", headers, "response body"); + } + + private Response nullBodyResponse() { + return Response.create(200, "OK", Collections.>emptyMap(), null); + } +} diff --git a/core/src/test/java/feign/codec/DefaultEncoderTest.java b/core/src/test/java/feign/codec/DefaultEncoderTest.java new file mode 100644 index 0000000000..21f93026fa --- /dev/null +++ b/core/src/test/java/feign/codec/DefaultEncoderTest.java @@ -0,0 +1,39 @@ +/* + * 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.codec; + +import feign.RequestTemplate; +import org.testng.annotations.Test; + +import java.util.Date; + +import static org.testng.Assert.assertEquals; + +public class DefaultEncoderTest { + private final Encoder encoder = new Encoder.Default(); + + @Test public void testEncodesStrings() throws Exception { + String content = "This is my content"; + RequestTemplate template = new RequestTemplate(); + encoder.encode(content, template); + assertEquals(template.body(), content); + } + + @Test(expectedExceptions = EncodeException.class, expectedExceptionsMessageRegExp = ".* is not a type supported by this encoder.") + public void testRefusesToEncodeOtherTypes() throws Exception { + encoder.encode(new Date(), new RequestTemplate()); + } +} diff --git a/core/src/test/java/feign/codec/SAXDecoderTest.java b/core/src/test/java/feign/codec/SAXDecoderTest.java index 01f0d75da1..f434302bab 100644 --- a/core/src/test/java/feign/codec/SAXDecoderTest.java +++ b/core/src/test/java/feign/codec/SAXDecoderTest.java @@ -17,6 +17,7 @@ import dagger.ObjectGraph; import dagger.Provides; +import feign.Response; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.xml.sax.helpers.DefaultHandler; @@ -24,11 +25,10 @@ import javax.inject.Inject; import javax.inject.Provider; import java.io.IOException; -import java.io.StringReader; import java.text.ParseException; -import java.util.Set; +import java.util.Collection; +import java.util.Collections; -import static dagger.Provides.Type.SET; import static org.testng.Assert.assertEquals; // unbound wildcards are not currently injectable in dagger. @@ -37,8 +37,8 @@ public class SAXDecoderTest { @dagger.Module(injects = SAXDecoderTest.class) static class Module { - @Provides(type = SET) Decoder saxDecoder(Provider networkStatus, // - Provider networkStatusAsString) { + @Provides Decoder saxDecoder(Provider networkStatus, // + Provider networkStatusAsString) { return SAXDecoder.builder() // .addContentHandler(networkStatus) // .addContentHandler(networkStatusAsString) // @@ -46,23 +46,25 @@ static class Module { } } - @Inject Set decoders; + @Inject Decoder decoder; @BeforeClass void inject() { ObjectGraph.create(new Module()).inject(this); } @Test public void parsesConfiguredTypes() throws ParseException, IOException { - Decoder decoder = decoders.iterator().next(); - assertEquals(decoder.decode(new StringReader(statusFailed), NetworkStatus.class), NetworkStatus.FAILED); - assertEquals(decoder.decode(new StringReader(statusFailed), String.class), "Failed"); + assertEquals(decoder.decode(statusFailedResponse(), NetworkStatus.class), NetworkStatus.FAILED); + assertEquals(decoder.decode(statusFailedResponse(), String.class), "Failed"); } @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "type int not in configured handlers \\[class .*NetworkStatus, class java.lang.String\\]") public void niceErrorOnUnconfiguredType() throws ParseException, IOException { - Decoder decoder = decoders.iterator().next(); - decoder.decode(new StringReader(statusFailed), int.class); + decoder.decode(statusFailedResponse(), int.class); + } + + private Response statusFailedResponse() { + return Response.create(200, "OK", Collections.>emptyMap(), statusFailed); } static String statusFailed = ""// diff --git a/core/src/test/java/feign/examples/GitHubExample.java b/core/src/test/java/feign/examples/GitHubExample.java index aaac375221..02223ad564 100644 --- a/core/src/test/java/feign/examples/GitHubExample.java +++ b/core/src/test/java/feign/examples/GitHubExample.java @@ -23,6 +23,7 @@ import feign.Feign; import feign.Logger; import feign.RequestLine; +import feign.Response; import feign.codec.Decoder; import javax.inject.Inject; @@ -33,7 +34,7 @@ import java.lang.reflect.Type; import java.util.List; -import static dagger.Provides.Type.SET; +import static feign.Util.ensureClosed; /** * adapted from {@code com.example.retrofit.GitHubClient} @@ -82,20 +83,28 @@ static class GsonModule { return new Gson(); } - @Provides(type = SET) Decoder decoder(GsonDecoder gsonDecoder) { + @Provides Decoder decoder(GsonDecoder gsonDecoder) { return gsonDecoder; } } - static class GsonDecoder implements Decoder.TextStream { + static class GsonDecoder implements Decoder { private final Gson gson; @Inject GsonDecoder(Gson gson) { this.gson = gson; } - @Override public Object decode(Reader reader, Type type) throws IOException { - return fromJson(new JsonReader(reader), type); + @Override public Object decode(Response response, Type type) throws IOException { + if (response.body() == null) { + return null; + } + Reader reader = response.body().asReader(); + try { + return fromJson(new JsonReader(reader), type); + } finally { + ensureClosed(reader); + } } private Object fromJson(JsonReader jsonReader, Type type) throws IOException { diff --git a/gson/src/main/java/feign/gson/GsonModule.java b/gson/src/main/java/feign/gson/GsonModule.java index 52fd8077d3..f7192d2409 100644 --- a/gson/src/main/java/feign/gson/GsonModule.java +++ b/gson/src/main/java/feign/gson/GsonModule.java @@ -26,8 +26,9 @@ import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import dagger.Provides; +import feign.RequestTemplate; +import feign.Response; import feign.codec.Decoder; -import feign.codec.EncodeException; import feign.codec.Encoder; import javax.inject.Inject; @@ -38,32 +39,40 @@ import java.util.Collections; import java.util.Map; -import static dagger.Provides.Type.SET; +import static feign.Util.ensureClosed; @dagger.Module(library = true) public final class GsonModule { - @Provides(type = SET) Encoder encoder(GsonCodec codec) { + @Provides Encoder encoder(GsonCodec codec) { return codec; } - @Provides(type = SET) Decoder decoder(GsonCodec codec) { + @Provides Decoder decoder(GsonCodec codec) { return codec; } - static class GsonCodec implements Encoder.Text, Decoder.TextStream { + static class GsonCodec implements Encoder, Decoder { private final Gson gson; @Inject GsonCodec(Gson gson) { this.gson = gson; } - @Override public String encode(Object object) throws EncodeException { - return gson.toJson(object); + @Override public void encode(Object object, RequestTemplate template) { + template.body(gson.toJson(object)); } - @Override public Object decode(Reader reader, Type type) throws IOException { - return fromJson(new JsonReader(reader), type); + @Override public Object decode(Response response, Type type) throws IOException { + if (response.body() == null) { + return null; + } + Reader reader = response.body().asReader(); + try { + return fromJson(new JsonReader(reader), type); + } finally { + ensureClosed(reader); + } } private Object fromJson(JsonReader jsonReader, Type type) throws IOException { diff --git a/gson/src/test/java/feign/gson/GsonModuleTest.java b/gson/src/test/java/feign/gson/GsonModuleTest.java index bde0f8d71d..0170036f5d 100644 --- a/gson/src/test/java/feign/gson/GsonModuleTest.java +++ b/gson/src/test/java/feign/gson/GsonModuleTest.java @@ -18,52 +18,54 @@ import com.google.gson.reflect.TypeToken; import dagger.Module; import dagger.ObjectGraph; +import feign.RequestTemplate; +import feign.Response; import feign.codec.Decoder; import feign.codec.Encoder; import org.testng.annotations.Test; import javax.inject.Inject; -import java.io.StringReader; import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Set; import static org.testng.Assert.assertEquals; @Test public class GsonModuleTest { - @Module(includes = GsonModule.class, library = true, injects = EncodersAndDecoders.class) - static class EncodersAndDecoders { - @Inject Set encoders; - @Inject Set decoders; + @Module(includes = GsonModule.class, library = true, injects = EncoderAndDecoderBindings.class) + static class EncoderAndDecoderBindings { + @Inject Encoder encoder; + @Inject Decoder decoder; } - @Test public void providesEncoderDecoderAndIncrementalDecoder() throws Exception { - EncodersAndDecoders bindings = new EncodersAndDecoders(); + @Test public void providesEncoderDecoder() throws Exception { + EncoderAndDecoderBindings bindings = new EncoderAndDecoderBindings(); ObjectGraph.create(bindings).inject(bindings); - assertEquals(bindings.encoders.size(), 1); - assertEquals(bindings.encoders.iterator().next().getClass(), GsonModule.GsonCodec.class); - assertEquals(bindings.decoders.size(), 1); - assertEquals(bindings.decoders.iterator().next().getClass(), GsonModule.GsonCodec.class); + assertEquals(bindings.encoder.getClass(), GsonModule.GsonCodec.class); + assertEquals(bindings.decoder.getClass(), GsonModule.GsonCodec.class); } - @Module(includes = GsonModule.class, library = true, injects = Encoders.class) - static class Encoders { - @Inject Set encoders; + @Module(includes = GsonModule.class, library = true, injects = EncoderBindings.class) + static class EncoderBindings { + @Inject Encoder encoder; } @Test public void encodesMapObjectNumericalValuesAsInteger() throws Exception { - Encoders bindings = new Encoders(); + EncoderBindings bindings = new EncoderBindings(); ObjectGraph.create(bindings).inject(bindings); Map map = new LinkedHashMap(); map.put("foo", 1); - assertEquals(Encoder.Text.class.cast(bindings.encoders.iterator().next()).encode(map), ""// + RequestTemplate template = new RequestTemplate(); + bindings.encoder.encode(map, template); + assertEquals(template.body(), ""// + "{\n" // + " \"foo\": 1\n" // + "}"); @@ -71,14 +73,16 @@ static class Encoders { @Test public void encodesFormParams() throws Exception { - Encoders bindings = new Encoders(); + EncoderBindings bindings = new EncoderBindings(); ObjectGraph.create(bindings).inject(bindings); Map form = new LinkedHashMap(); form.put("foo", 1); form.put("bar", Arrays.asList(2, 3)); - assertEquals(Encoder.Text.class.cast(bindings.encoders.iterator().next()).encode(form), ""// + RequestTemplate template = new RequestTemplate(); + bindings.encoder.encode(form, template); + assertEquals(template.body(), ""// + "{\n" // + " \"foo\": 1,\n" // + " \"bar\": [\n" // @@ -106,22 +110,22 @@ static class Zone extends LinkedHashMap { private static final long serialVersionUID = 1L; } - @Module(includes = GsonModule.class, library = true, injects = Decoders.class) - static class Decoders { - @Inject Set decoders; + @Module(includes = GsonModule.class, library = true, injects = DecoderBindings.class) + static class DecoderBindings { + @Inject Decoder decoder; } @Test public void decodes() throws Exception { - Decoders bindings = new Decoders(); + DecoderBindings bindings = new DecoderBindings(); ObjectGraph.create(bindings).inject(bindings); List zones = new LinkedList(); zones.add(new Zone("denominator.io.")); zones.add(new Zone("denominator.io.", "ABCD")); - assertEquals(Decoder.TextStream.class.cast(bindings.decoders.iterator().next()) - .decode(new StringReader(zonesJson), new TypeToken>() { - }.getType()), zones); + Response response = Response.create(200, "OK", Collections.>emptyMap(), zonesJson); + assertEquals(bindings.decoder.decode(response, new TypeToken>() { + }.getType()), zones); } private String zonesJson = ""// From 99760f750a40f4ffb6d1619946f75522511b8308 Mon Sep 17 00:00:00 2001 From: "David M. Carr" Date: Fri, 13 Sep 2013 16:08:46 -0400 Subject: [PATCH 101/179] Fix default decoder's support for Response decoding, clean up usage of StringDecoder Decoder.Default's decoding to Response didn't actually work; the reader would always be closed when used from Feign, as it depended on the url connection, which would have been closed by the time the Response object was returned to the client. This wasn't noticed because the default decoder tests don't use the mock web server. There will be test coverage added for this shortly as part of the enhancements to support a Builder. --- core/src/main/java/feign/FeignException.java | 7 +----- core/src/main/java/feign/Util.java | 22 +++++++++++++++++++ core/src/main/java/feign/codec/Decoder.java | 13 ++++++----- .../main/java/feign/codec/StringDecoder.java | 21 ++---------------- .../java/feign/codec/DefaultDecoderTest.java | 13 ++++++----- 5 files changed, 41 insertions(+), 35 deletions(-) diff --git a/core/src/main/java/feign/FeignException.java b/core/src/main/java/feign/FeignException.java index c158aeb2e5..9d47141088 100644 --- a/core/src/main/java/feign/FeignException.java +++ b/core/src/main/java/feign/FeignException.java @@ -19,8 +19,6 @@ import java.io.IOException; -import feign.codec.StringDecoder; - /** * Origin exception type for all Http Apis. */ @@ -29,14 +27,11 @@ static FeignException errorReading(Request request, Response response, IOExcepti return new FeignException(format("%s %s %s", cause.getMessage(), request.method(), request.url(), 0), cause); } - private static final StringDecoder toString = new StringDecoder(); - public static FeignException errorStatus(String methodKey, Response response) { String message = format("status %s reading %s", response.status(), methodKey); try { if (response.body() != null) { - String body = toString.decode(response, String.class).toString(); - response = Response.create(response.status(), response.reason(), response.headers(), body); + String body = Util.toString(response.body().asReader()); message += "; content:\n" + body; } } catch (IOException ignored) { // NOPMD diff --git a/core/src/main/java/feign/Util.java b/core/src/main/java/feign/Util.java index 289f60f79b..412b10f66c 100644 --- a/core/src/main/java/feign/Util.java +++ b/core/src/main/java/feign/Util.java @@ -17,10 +17,12 @@ import java.io.Closeable; import java.io.IOException; +import java.io.Reader; import java.lang.reflect.Array; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.WildcardType; +import java.nio.CharBuffer; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; @@ -163,4 +165,24 @@ public static Type resolveLastTypeParameter(Type genericContext, Class supert } return types[types.length - 1]; } + + private static final int BUF_SIZE = 0x800; // 2K chars (4K bytes) + + public static String toString(Reader reader) throws IOException { + if (reader == null) { + return null; + } + try { + StringBuilder to = new StringBuilder(); + CharBuffer buf = CharBuffer.allocate(BUF_SIZE); + while (reader.read(buf) != -1) { + buf.flip(); + to.append(buf); + buf.clear(); + } + return to.toString(); + } finally { + ensureClosed(reader); + } + } } diff --git a/core/src/main/java/feign/codec/Decoder.java b/core/src/main/java/feign/codec/Decoder.java index 1a7865cab5..54f078fc50 100644 --- a/core/src/main/java/feign/codec/Decoder.java +++ b/core/src/main/java/feign/codec/Decoder.java @@ -17,6 +17,7 @@ import feign.FeignException; import feign.Response; +import feign.Util; import java.io.IOException; import java.lang.reflect.Type; @@ -74,16 +75,18 @@ public interface Decoder { * signatures. */ public class Default implements Decoder { - private final StringDecoder stringDecoder = new StringDecoder(); - @Override public Object decode(Response response, Type type) throws IOException { if (Response.class.equals(type)) { - return response; - } else if (String.class.equals(type)) { - return stringDecoder.decode(response, type); + String bodyString = null; + if (response.body() != null) { + bodyString = Util.toString(response.body().asReader()); + } + return Response.create(response.status(), response.reason(), response.headers(), bodyString); } else if (void.class.equals(type) || response.body() == null) { return null; + } else if (String.class.equals(type)) { + return Util.toString(response.body().asReader()); } throw new DecodeException(format("%s is not a type supported by this decoder.", type)); } diff --git a/core/src/main/java/feign/codec/StringDecoder.java b/core/src/main/java/feign/codec/StringDecoder.java index 93f66eac82..03c1b1d3a6 100644 --- a/core/src/main/java/feign/codec/StringDecoder.java +++ b/core/src/main/java/feign/codec/StringDecoder.java @@ -16,38 +16,21 @@ package feign.codec; import feign.Response; +import feign.Util; import java.io.IOException; -import java.io.Reader; import java.lang.reflect.Type; -import java.nio.CharBuffer; - -import static feign.Util.ensureClosed; /** * Adapted from {@code com.google.common.io.CharStreams.toString()}. */ public class StringDecoder implements Decoder { - private static final int BUF_SIZE = 0x800; // 2K chars (4K bytes) - @Override public Object decode(Response response, Type type) throws IOException { Response.Body body = response.body(); if (body == null) { return null; } - Reader from = body.asReader(); - try { - StringBuilder to = new StringBuilder(); - CharBuffer buf = CharBuffer.allocate(BUF_SIZE); - while (from.read(buf) != -1) { - buf.flip(); - to.append(buf); - buf.clear(); - } - return to.toString(); - } finally { - ensureClosed(from); - } + return Util.toString(body.asReader()); } } diff --git a/core/src/test/java/feign/codec/DefaultDecoderTest.java b/core/src/test/java/feign/codec/DefaultDecoderTest.java index d02f8a240c..9442d760f0 100644 --- a/core/src/test/java/feign/codec/DefaultDecoderTest.java +++ b/core/src/test/java/feign/codec/DefaultDecoderTest.java @@ -15,11 +15,12 @@ */ package feign.codec; -import feign.FeignException; import feign.Response; +import feign.Util; import org.testng.annotations.Test; import org.w3c.dom.Document; +import java.io.StringReader; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -38,19 +39,19 @@ public class DefaultDecoderTest { @Test public void testDecodesToResponse() throws Exception { Response response = knownResponse(); Object decodedObject = decoder.decode(response, Response.class); - assertEquals(decodedObject.getClass(), Response.class, ""); + assertEquals(decodedObject.getClass(), Response.class); Response decodedResponse = (Response) decodedObject; assertEquals(decodedResponse.status(), response.status()); assertEquals(decodedResponse.reason(), response.reason()); assertEquals(decodedResponse.headers(), response.headers()); - assertEquals(decodedResponse.body().toString(), response.body().toString()); + assertEquals(Util.toString(decodedResponse.body().asReader()), "response body"); } @Test public void testDecodesToString() throws Exception { Response response = knownResponse(); Object decodedObject = decoder.decode(response, String.class); assertEquals(decodedObject.getClass(), String.class); - assertEquals(decodedObject.toString(), response.body().toString()); + assertEquals(decodedObject.toString(), "response body"); } @Test public void testDecodesNullBodyToNull() throws Exception { @@ -63,9 +64,11 @@ public void testRefusesToDecodeOtherTypes() throws Exception { } private Response knownResponse() { + String content = "response body"; + StringReader reader = new StringReader(content); Map> headers = new HashMap>(); headers.put("Content-Type", Collections.singleton("text/plain")); - return Response.create(200, "OK", headers, "response body"); + return Response.create(200, "OK", headers, reader, content.length()); } private Response nullBodyResponse() { From c2749f1631194ad6bb9313b0f06e7eee3c5da5ea Mon Sep 17 00:00:00 2001 From: "David M. Carr" Date: Fri, 13 Sep 2013 17:08:27 -0400 Subject: [PATCH 102/179] Add Feign.Builder (#34) For those who do not use Dagger, or do not wish to, this provides an alternate method of defining dependencies. This includes logging config, decoders, etc. It still uses Dagger under the scenes, but doesn't require the user to deal with the module system. --- CHANGES.md | 1 + README.md | 24 ++- core/src/main/java/feign/Feign.java | 169 +++++++++++++++++- .../src/test/java/feign/FeignBuilderTest.java | 126 +++++++++++++ 4 files changed, 312 insertions(+), 8 deletions(-) create mode 100644 core/src/test/java/feign/FeignBuilderTest.java diff --git a/CHANGES.md b/CHANGES.md index fc3771644b..023a13f72f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ * Remove pattern decoders in favor of SaxDecoder. * Use single non-generic Decoder/Encoder instead of sets of type-specific Decoders/Encoders. * Decoders/Encoders are now more flexible, having access to the Response/RequestTemplate respectively. +* Added Feign.Builder to simplify client customizations without using Dagger. ### Version 4.4.1 * Fix NullPointerException on calling equals and hashCode. diff --git a/README.md b/README.md index 7a67d04e96..57d0c4754c 100644 --- a/README.md +++ b/README.md @@ -40,23 +40,33 @@ 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. +### Customization + +Feign has several aspects that can be customized. For simple cases, you can use `Feign.builder()` to construct an API interface with your custom components. For example: + +```java +interface Bank { + @RequestLine("POST /account/{id}") + Account getAccountInfo(@Named("id") String id); +} +... +Bank bank = Feign.builder().decoder(new AccountDecoder()).target(Bank.class, "https://api.examplebank.com"); +``` + +For further flexibility, you can use Dagger modules directly. See the `Dagger` section for more details. + ### 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()); +Bank bank = Feign.builder().decoder(accountDecoder).requestInterceptor(new ForwardedForInterceptor()).target(Bank.class, "https://api.examplebank.com"); ``` ### Multiple Interfaces @@ -65,7 +75,7 @@ Feign can produce multiple api interfaces. These are defined as `Target` (de For example, the following pattern might decorate each request with the current url and auth token from the identity service. ```java -CloudDNS cloudDNS = Feign.create().newInstance(new CloudIdentityTarget(user, apiKey)); +CloudDNS cloudDNS = Feign.builder().target(new CloudIdentityTarget(user, apiKey)); ``` You can find [several examples](https://github.com/Netflix/feign/tree/master/feign-core/src/test/java/feign/examples) in the test tree. Do take time to look at them, as seeing is believing! diff --git a/core/src/main/java/feign/Feign.java b/core/src/main/java/feign/Feign.java index 6bbf4715a3..820ea26097 100644 --- a/core/src/main/java/feign/Feign.java +++ b/core/src/main/java/feign/Feign.java @@ -16,6 +16,7 @@ package feign; +import dagger.Module; import dagger.ObjectGraph; import dagger.Provides; import feign.Logger.NoOpLogger; @@ -25,12 +26,15 @@ import feign.codec.Encoder; import feign.codec.ErrorDecoder; +import javax.inject.Inject; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLSocketFactory; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; /** * Feign's purpose is to ease development against http apis that feign @@ -48,6 +52,10 @@ public abstract class Feign { */ public abstract T newInstance(Target target); + public static Builder builder() { + return new Builder(); + } + public static T create(Class apiType, String url, Object... modules) { return create(new HardCodedTarget(apiType, url), modules); } @@ -78,7 +86,7 @@ public static ObjectGraph createObjectGraph(Object... modules) { } @SuppressWarnings("rawtypes") - @dagger.Module(complete = false, injects = Feign.class, library = true) + @dagger.Module(complete = false, injects = {Feign.class, Builder.class}, library = true) public static class Defaults { @Provides Logger.Level logLevel() { @@ -168,4 +176,163 @@ private static List modulesForGraph(Object... modules) { modulesForGraph.add(module); return modulesForGraph; } + + public static class Builder { + private final Set requestInterceptors = new LinkedHashSet(); + @Inject Logger.Level logLevel; + @Inject Contract contract; + @Inject Client client; + @Inject Retryer retryer; + @Inject Logger logger; + @Inject Encoder encoder; + @Inject Decoder decoder; + @Inject ErrorDecoder errorDecoder; + @Inject Options options; + + Builder() { + ObjectGraph.create(new Defaults()).inject(this); + } + + public Builder logLevel(Logger.Level logLevel) { + this.logLevel = logLevel; + return this; + } + + public Builder contract(Contract contract) { + this.contract = contract; + return this; + } + + public Builder client(Client client) { + this.client = client; + return this; + } + + public Builder retryer(Retryer retryer) { + this.retryer = retryer; + return this; + } + + public Builder logger(Logger logger) { + this.logger = logger; + return this; + } + + public Builder encoder(Encoder encoder) { + this.encoder = encoder; + return this; + } + + public Builder decoder(Decoder decoder) { + this.decoder = decoder; + return this; + } + + public Builder errorDecoder(ErrorDecoder errorDecoder) { + this.errorDecoder = errorDecoder; + return this; + } + + public Builder options(Options options) { + this.options = options; + return this; + } + + /** + * Adds a single request interceptor to the builder. + */ + public Builder requestInterceptor(RequestInterceptor requestInterceptor) { + this.requestInterceptors.add(requestInterceptor); + return this; + } + + /** + * Sets the full set of request interceptors for the builder, overwriting any previous interceptors. + */ + public Builder requestInterceptors(Iterable requestInterceptors) { + this.requestInterceptors.clear(); + for (RequestInterceptor requestInterceptor : requestInterceptors) { + this.requestInterceptors.add(requestInterceptor); + } + return this; + } + + public T target(Class apiType, String url) { + return target(new HardCodedTarget(apiType, url)); + } + + public T target(Target target) { + BuilderModule module = new BuilderModule(this); + return create(module).newInstance(target); + } + } + + @Module(library = true, overrides = true, addsTo = Defaults.class) + static class BuilderModule { + private final Logger.Level logLevel; + private final Contract contract; + private final Client client; + private final Retryer retryer; + private final Logger logger; + private final Encoder encoder; + private final Decoder decoder; + private final ErrorDecoder errorDecoder; + private final Options options; + private final Set requestInterceptors; + + BuilderModule(Builder builder) { + this.logLevel = builder.logLevel; + this.contract = builder.contract; + this.client = builder.client; + this.retryer = builder.retryer; + this.logger = builder.logger; + this.encoder = builder.encoder; + this.decoder = builder.decoder; + this.errorDecoder = builder.errorDecoder; + this.options = builder.options; + this.requestInterceptors = builder.requestInterceptors; + } + + @Provides Logger.Level logLevel() { + return logLevel; + } + + @Provides Contract contract() { + return contract; + } + + @Provides Client client() { + return client; + } + + @Provides Retryer retryer() { + return retryer; + } + + @Provides Logger logger() { + return logger; + } + + @Provides + Encoder encoder() { + return encoder; + } + + @Provides + Decoder decoder() { + return decoder; + } + + @Provides ErrorDecoder errorDecoder() { + return errorDecoder; + } + + @Provides Options options() { + return options; + } + + @Provides(type = Provides.Type.SET_VALUES) Set requestInterceptors() { + return requestInterceptors; + } + } } diff --git a/core/src/test/java/feign/FeignBuilderTest.java b/core/src/test/java/feign/FeignBuilderTest.java new file mode 100644 index 0000000000..097e80aca3 --- /dev/null +++ b/core/src/test/java/feign/FeignBuilderTest.java @@ -0,0 +1,126 @@ +/* + * 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.mockwebserver.MockResponse; +import com.google.mockwebserver.MockWebServer; +import com.google.mockwebserver.RecordedRequest; +import feign.codec.Decoder; +import feign.codec.EncodeException; +import feign.codec.Encoder; +import org.testng.annotations.Test; + +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.List; + +import static org.testng.Assert.assertEquals; + +public class FeignBuilderTest { + interface TestInterface { + @RequestLine("POST /") Response codecPost(String data); + + @RequestLine("POST /") void encodedPost(List data); + + @RequestLine("POST /") String decodedPost(); + } + + @Test public void testDefaults() throws Exception { + MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse().setBody("response data")); + server.play(); + + String url = "http://localhost:" + server.getPort(); + try { + TestInterface api = Feign.builder().target(TestInterface.class, url); + Response response = api.codecPost("request data"); + assertEquals(Util.toString(response.body().asReader()), "response data"); + } finally { + server.shutdown(); + assertEquals(server.getRequestCount(), 1); + assertEquals(server.takeRequest().getUtf8Body(), "request data"); + } + } + + @Test public void testOverrideEncoder() throws Exception { + MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse().setBody("response data")); + server.play(); + + String url = "http://localhost:" + server.getPort(); + Encoder encoder = new Encoder() { + @Override + public void encode(Object object, RequestTemplate template) throws EncodeException { + template.body(object.toString()); + } + }; + try { + TestInterface api = Feign.builder().encoder(encoder).target(TestInterface.class, url); + api.encodedPost(Arrays.asList("This", "is", "my", "request")); + } finally { + server.shutdown(); + assertEquals(server.getRequestCount(), 1); + assertEquals(server.takeRequest().getUtf8Body(), "[This, is, my, request]"); + } + } + + @Test public void testOverrideDecoder() throws Exception { + MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse().setBody("success!")); + server.play(); + + String url = "http://localhost:" + server.getPort(); + Decoder decoder = new Decoder() { + @Override + public Object decode(Response response, Type type) { + return "fail"; + } + }; + + try { + TestInterface api = Feign.builder().decoder(decoder).target(TestInterface.class, url); + assertEquals(api.decodedPost(), "fail"); + } finally { + server.shutdown(); + assertEquals(server.getRequestCount(), 1); + } + } + + @Test public void testProvideRequestInterceptors() throws Exception { + MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse().setBody("response data")); + server.play(); + + String url = "http://localhost:" + server.getPort(); + RequestInterceptor requestInterceptor = new RequestInterceptor() { + @Override + public void apply(RequestTemplate template) { + template.header("Content-Type", "text/plain"); + } + }; + try { + TestInterface api = Feign.builder().requestInterceptor(requestInterceptor).target(TestInterface.class, url); + Response response = api.codecPost("request data"); + assertEquals(Util.toString(response.body().asReader()), "response data"); + } finally { + server.shutdown(); + assertEquals(server.getRequestCount(), 1); + RecordedRequest request = server.takeRequest(); + assertEquals(request.getUtf8Body(), "request data"); + assertEquals(request.getHeader("Content-Type"), "text/plain"); + } + } +} From 58aa3bcb748d8445b2ad595d1c0b5eea1f7ec3bd Mon Sep 17 00:00:00 2001 From: adriancole Date: Sun, 15 Sep 2013 10:53:26 +0200 Subject: [PATCH 103/179] Gson type adapters can be registered as Dagger set bindings. --- CHANGES.md | 1 + gson/src/main/java/feign/gson/GsonModule.java | 57 ++++++++++++++++--- .../test/java/feign/gson/GsonModuleTest.java | 42 ++++++++++++++ 3 files changed, 93 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 023a13f72f..49a5347497 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ * Use single non-generic Decoder/Encoder instead of sets of type-specific Decoders/Encoders. * Decoders/Encoders are now more flexible, having access to the Response/RequestTemplate respectively. * Added Feign.Builder to simplify client customizations without using Dagger. +* Gson type adapters can be registered as Dagger set bindings. ### Version 4.4.1 * Fix NullPointerException on calling equals and hashCode. diff --git a/gson/src/main/java/feign/gson/GsonModule.java b/gson/src/main/java/feign/gson/GsonModule.java index f7192d2409..16fad54856 100644 --- a/gson/src/main/java/feign/gson/GsonModule.java +++ b/gson/src/main/java/feign/gson/GsonModule.java @@ -38,9 +38,47 @@ import java.lang.reflect.Type; import java.util.Collections; import java.util.Map; +import java.util.Set; import static feign.Util.ensureClosed; - +import static feign.Util.resolveLastTypeParameter; + +/** + *

Custom type adapters

+ *
+ * In order to specify custom json parsing, + * {@code Gson} supports {@link TypeAdapter type adapters}. This module adds one + * to read numbers in a {@code Map} as Integers. You can + * customize further by adding additional set bindings to the raw type + * {@code TypeAdapter}. + * + *
+ * Here's an example of adding a custom json type adapter. + * + *
+ * @Provides(type = Provides.Type.SET)
+ * TypeAdapter upperZone() {
+ *     return new TypeAdapter<Zone>() {
+ * 
+ *         @Override
+ *         public void write(JsonWriter out, Zone value) throws IOException {
+ *             throw new IllegalArgumentException();
+ *         }
+ * 
+ *         @Override
+ *         public Zone read(JsonReader in) throws IOException {
+ *             in.beginObject();
+ *             Zone zone = new Zone();
+ *             while (in.hasNext()) {
+ *                 zone.put(in.nextName(), in.nextString().toUpperCase());
+ *             }
+ *             in.endObject();
+ *             return zone;
+ *         }
+ *     };
+ * }
+ * 
+ */ @dagger.Module(library = true) public final class GsonModule { @@ -87,8 +125,17 @@ private Object fromJson(JsonReader jsonReader, Type type) throws IOException { } } + @Provides @Singleton Gson gson(Set adapters) { + GsonBuilder builder = new GsonBuilder().setPrettyPrinting(); + for (TypeAdapter adapter : adapters) { + Type type = resolveLastTypeParameter(adapter.getClass(), TypeAdapter.class); + builder.registerTypeAdapter(type, adapter); + } + return builder.create(); + } + // deals with scenario where gson Object type treats all numbers as doubles. - @Provides TypeAdapter> doubleToInt() { + @Provides(type = Provides.Type.SET) TypeAdapter doubleToInt() { return new TypeAdapter>() { TypeAdapter> delegate = new MapTypeAdapterFactory(new ConstructorConstructor( Collections.>emptyMap()), false).create(new Gson(), token); @@ -111,10 +158,6 @@ public Map read(JsonReader in) throws IOException { }.nullSafe(); } - @Provides @Singleton Gson gson(TypeAdapter> doubleToInt) { - return new GsonBuilder().registerTypeAdapter(token.getType(), doubleToInt).setPrettyPrinting().create(); - } - - protected final static TypeToken> token = new TypeToken>() { + private final static TypeToken> token = new TypeToken>() { }; } diff --git a/gson/src/test/java/feign/gson/GsonModuleTest.java b/gson/src/test/java/feign/gson/GsonModuleTest.java index 0170036f5d..0ecd61e59a 100644 --- a/gson/src/test/java/feign/gson/GsonModuleTest.java +++ b/gson/src/test/java/feign/gson/GsonModuleTest.java @@ -15,9 +15,13 @@ */ package feign.gson; +import com.google.gson.TypeAdapter; import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; import dagger.Module; import dagger.ObjectGraph; +import dagger.Provides; import feign.RequestTemplate; import feign.Response; import feign.codec.Decoder; @@ -25,6 +29,7 @@ import org.testng.annotations.Test; import javax.inject.Inject; +import java.io.IOException; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -138,4 +143,41 @@ static class DecoderBindings { + " \"id\": \"ABCD\"\n"// + " }\n"// + "]\n"; + + @Module(includes = GsonModule.class, library = true, injects = CustomTypeAdapter.class) + static class CustomTypeAdapter { + @Provides(type = Provides.Type.SET) TypeAdapter upperZone() { + return new TypeAdapter() { + + @Override public void write(JsonWriter out, Zone value) throws IOException { + throw new IllegalArgumentException(); + } + + @Override public Zone read(JsonReader in) throws IOException { + in.beginObject(); + Zone zone = new Zone(); + while (in.hasNext()) { + zone.put(in.nextName(), in.nextString().toUpperCase()); + } + in.endObject(); + return zone; + } + }; + } + + @Inject Decoder decoder; + } + + @Test public void customDecoder() throws Exception { + CustomTypeAdapter bindings = new CustomTypeAdapter(); + ObjectGraph.create(bindings).inject(bindings); + + List zones = new LinkedList(); + zones.add(new Zone("DENOMINATOR.IO.")); + zones.add(new Zone("DENOMINATOR.IO.", "ABCD")); + + Response response = Response.create(200, "OK", Collections.>emptyMap(), zonesJson); + assertEquals(bindings.decoder.decode(response, new TypeToken>() { + }.getType()), zones); + } } From 2fc54d5d0037847167bf56116e7e35c59be69135 Mon Sep 17 00:00:00 2001 From: adriancole Date: Sun, 15 Sep 2013 11:48:34 +0200 Subject: [PATCH 104/179] New Defaults.WithoutCodec to avoid binding collisions. --- CHANGES.md | 1 + core/src/main/java/feign/Feign.java | 80 ++++++++++++++++------------- 2 files changed, 44 insertions(+), 37 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 49a5347497..bd0edf11af 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ * Decoders/Encoders are now more flexible, having access to the Response/RequestTemplate respectively. * Added Feign.Builder to simplify client customizations without using Dagger. * Gson type adapters can be registered as Dagger set bindings. +* New Defaults.WithoutCodec to avoid binding collisions. ### Version 4.4.1 * Fix NullPointerException on calling equals and hashCode. diff --git a/core/src/main/java/feign/Feign.java b/core/src/main/java/feign/Feign.java index 820ea26097..af322f69bf 100644 --- a/core/src/main/java/feign/Feign.java +++ b/core/src/main/java/feign/Feign.java @@ -86,53 +86,61 @@ public static ObjectGraph createObjectGraph(Object... modules) { } @SuppressWarnings("rawtypes") - @dagger.Module(complete = false, injects = {Feign.class, Builder.class}, library = true) + @dagger.Module(complete = false, injects = {Feign.class, Builder.class}, + includes = {Defaults.WithoutCodec.class, Defaults.Codec.class}, library = true) public static class Defaults { - @Provides Logger.Level logLevel() { - return Logger.Level.NONE; - } + @dagger.Module(includes = {Defaults.Client.class}, library = true) + public static class WithoutCodec { + @Provides Contract contract() { + return new Contract.Default(); + } - @Provides Contract contract() { - return new Contract.Default(); - } + @Provides Logger.Level logLevel() { + return Logger.Level.NONE; + } - @Provides SSLSocketFactory sslSocketFactory() { - return SSLSocketFactory.class.cast(SSLSocketFactory.getDefault()); - } + @Provides Logger noOp() { + return new NoOpLogger(); + } - @Provides HostnameVerifier hostnameVerifier() { - return HttpsURLConnection.getDefaultHostnameVerifier(); - } + @Provides Retryer retryer() { + return new Retryer.Default(); + } - @Provides Client httpClient(Client.Default client) { - return client; + @Provides ErrorDecoder errorDecoder() { + return new ErrorDecoder.Default(); + } } - @Provides Retryer retryer() { - return new Retryer.Default(); - } + @dagger.Module(library = true) + public static class Client { + @Provides SSLSocketFactory sslSocketFactory() { + return SSLSocketFactory.class.cast(SSLSocketFactory.getDefault()); + } - @Provides Logger noOp() { - return new NoOpLogger(); - } + @Provides HostnameVerifier hostnameVerifier() { + return HttpsURLConnection.getDefaultHostnameVerifier(); + } - @Provides - Encoder defaultEncoder() { - return new Encoder.Default(); - } + @Provides feign.Client httpClient(feign.Client.Default client) { + return client; + } - @Provides - Decoder defaultDecoder() { - return new Decoder.Default(); + @Provides Options options() { + return new Options(); + } } - @Provides ErrorDecoder errorDecoder() { - return new ErrorDecoder.Default(); - } + @dagger.Module(library = true) + public static class Codec { + @Provides Encoder defaultEncoder() { + return new Encoder.Default(); + } - @Provides Options options() { - return new Options(); + @Provides Decoder defaultDecoder() { + return new Decoder.Default(); + } } } @@ -313,13 +321,11 @@ static class BuilderModule { return logger; } - @Provides - Encoder encoder() { + @Provides Encoder encoder() { return encoder; } - @Provides - Decoder decoder() { + @Provides Decoder decoder() { return decoder; } From 92c0c36af0c2cfb32b62ddf5e19d4e94423ffb53 Mon Sep 17 00:00:00 2001 From: adriancole Date: Sun, 15 Sep 2013 23:33:09 +0200 Subject: [PATCH 105/179] provide encoder/decoder in a module that addsTo = Feign.Defaults.class --- CHANGES.md | 2 +- core/src/main/java/feign/Feign.java | 109 +++++------------- core/src/test/java/feign/FeignTest.java | 16 ++- core/src/test/java/feign/LoggerTest.java | 13 ++- gson/src/main/java/feign/gson/GsonModule.java | 3 +- .../test/java/feign/gson/GsonModuleTest.java | 8 +- .../feign/gson/examples/GitHubExample.java | 49 ++++++++ .../feign/ribbon/LoadBalancingTargetTest.java | 2 +- .../java/feign/ribbon/RibbonClientTest.java | 22 +++- 9 files changed, 122 insertions(+), 102 deletions(-) create mode 100644 gson/src/test/java/feign/gson/examples/GitHubExample.java diff --git a/CHANGES.md b/CHANGES.md index bd0edf11af..c31e9f4973 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,7 +6,7 @@ * Decoders/Encoders are now more flexible, having access to the Response/RequestTemplate respectively. * Added Feign.Builder to simplify client customizations without using Dagger. * Gson type adapters can be registered as Dagger set bindings. -* New Defaults.WithoutCodec to avoid binding collisions. +* `Feign.create(...)` now requires specifying an encoder and decoder. ### Version 4.4.1 * Fix NullPointerException on calling equals and hashCode. diff --git a/core/src/main/java/feign/Feign.java b/core/src/main/java/feign/Feign.java index af322f69bf..c08bf16312 100644 --- a/core/src/main/java/feign/Feign.java +++ b/core/src/main/java/feign/Feign.java @@ -16,7 +16,6 @@ package feign; -import dagger.Module; import dagger.ObjectGraph; import dagger.Provides; import feign.Logger.NoOpLogger; @@ -86,61 +85,43 @@ public static ObjectGraph createObjectGraph(Object... modules) { } @SuppressWarnings("rawtypes") - @dagger.Module(complete = false, injects = {Feign.class, Builder.class}, - includes = {Defaults.WithoutCodec.class, Defaults.Codec.class}, library = true) + // incomplete as missing Encoder/Decoder + @dagger.Module(injects = {Feign.class, Builder.class}, complete = false, includes = ReflectiveFeign.Module.class) public static class Defaults { + @Provides Contract contract() { + return new Contract.Default(); + } - @dagger.Module(includes = {Defaults.Client.class}, library = true) - public static class WithoutCodec { - @Provides Contract contract() { - return new Contract.Default(); - } - - @Provides Logger.Level logLevel() { - return Logger.Level.NONE; - } - - @Provides Logger noOp() { - return new NoOpLogger(); - } - - @Provides Retryer retryer() { - return new Retryer.Default(); - } + @Provides Logger.Level logLevel() { + return Logger.Level.NONE; + } - @Provides ErrorDecoder errorDecoder() { - return new ErrorDecoder.Default(); - } + @Provides Logger noOp() { + return new NoOpLogger(); } - @dagger.Module(library = true) - public static class Client { - @Provides SSLSocketFactory sslSocketFactory() { - return SSLSocketFactory.class.cast(SSLSocketFactory.getDefault()); - } + @Provides Retryer retryer() { + return new Retryer.Default(); + } - @Provides HostnameVerifier hostnameVerifier() { - return HttpsURLConnection.getDefaultHostnameVerifier(); - } + @Provides ErrorDecoder errorDecoder() { + return new ErrorDecoder.Default(); + } - @Provides feign.Client httpClient(feign.Client.Default client) { - return client; - } + @Provides Options options() { + return new Options(); + } - @Provides Options options() { - return new Options(); - } + @Provides SSLSocketFactory sslSocketFactory() { + return SSLSocketFactory.class.cast(SSLSocketFactory.getDefault()); } - @dagger.Module(library = true) - public static class Codec { - @Provides Encoder defaultEncoder() { - return new Encoder.Default(); - } + @Provides HostnameVerifier hostnameVerifier() { + return HttpsURLConnection.getDefaultHostnameVerifier(); + } - @Provides Decoder defaultDecoder() { - return new Decoder.Default(); - } + @Provides feign.Client httpClient(feign.Client.Default client) { + return client; } } @@ -176,15 +157,15 @@ public static String configKey(Method method) { } private static List modulesForGraph(Object... modules) { - List modulesForGraph = new ArrayList(3); + List modulesForGraph = new ArrayList(2); modulesForGraph.add(new Defaults()); - modulesForGraph.add(new ReflectiveFeign.Module()); if (modules != null) for (Object module : modules) modulesForGraph.add(module); return modulesForGraph; } + @dagger.Module(injects = Feign.class, includes = ReflectiveFeign.Module.class) public static class Builder { private final Set requestInterceptors = new LinkedHashSet(); @Inject Logger.Level logLevel; @@ -192,8 +173,8 @@ public static class Builder { @Inject Client client; @Inject Retryer retryer; @Inject Logger logger; - @Inject Encoder encoder; - @Inject Decoder decoder; + Encoder encoder = new Encoder.Default(); + Decoder decoder = new Decoder.Default(); @Inject ErrorDecoder errorDecoder; @Inject Options options; @@ -270,35 +251,7 @@ public T target(Class apiType, String url) { } public T target(Target target) { - BuilderModule module = new BuilderModule(this); - return create(module).newInstance(target); - } - } - - @Module(library = true, overrides = true, addsTo = Defaults.class) - static class BuilderModule { - private final Logger.Level logLevel; - private final Contract contract; - private final Client client; - private final Retryer retryer; - private final Logger logger; - private final Encoder encoder; - private final Decoder decoder; - private final ErrorDecoder errorDecoder; - private final Options options; - private final Set requestInterceptors; - - BuilderModule(Builder builder) { - this.logLevel = builder.logLevel; - this.contract = builder.contract; - this.client = builder.client; - this.retryer = builder.retryer; - this.logger = builder.logger; - this.encoder = builder.encoder; - this.decoder = builder.decoder; - this.errorDecoder = builder.errorDecoder; - this.options = builder.options; - this.requestInterceptors = builder.requestInterceptors; + return ObjectGraph.create(this).get(Feign.class).newInstance(target); } @Provides Logger.Level logLevel() { diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index 58fdc54b67..873ac6258c 100644 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -73,8 +73,12 @@ void login( @RequestLine("GET /?1={1}&2={2}") Response queryParams(@Named("1") String one, @Named("2") Iterable twos); - @dagger.Module(overrides = true, library = true) + @dagger.Module(injects = Feign.class, addsTo = Feign.Defaults.class) static class Module { + @Provides Decoder defaultDecoder() { + return new Decoder.Default(); + } + @Provides Encoder defaultEncoder() { return new Encoder() { @Override public void encode(Object object, RequestTemplate template) { @@ -400,7 +404,7 @@ public void doesntRetryAfterResponseIsSent() throws IOException, InterruptedExce } } - @Module(injects = Client.Default.class, overrides = true, addsTo = Feign.Defaults.class) + @Module(overrides = true, includes = TestInterface.Module.class) static class TrustSSLSockets { @Provides SSLSocketFactory trustingSSLSocketFactory() { return TrustingSSLSocketFactory.get(); @@ -415,14 +419,14 @@ static class TrustSSLSockets { try { TestInterface api = Feign.create(TestInterface.class, "https://localhost:" + server.getPort(), - new TestInterface.Module(), new TrustSSLSockets()); + new TrustSSLSockets()); api.post(); } finally { server.shutdown(); } } - @Module(injects = Client.Default.class, overrides = true, addsTo = Feign.Defaults.class) + @Module(overrides = true, includes = TrustSSLSockets.class) static class DisableHostnameVerification { @Provides HostnameVerifier acceptAllHostnameVerifier() { return new AcceptAllHostnameVerifier(); @@ -437,7 +441,7 @@ static class DisableHostnameVerification { try { TestInterface api = Feign.create(TestInterface.class, "https://localhost:" + server.getPort(), - new TestInterface.Module(), new TrustSSLSockets(), new DisableHostnameVerification()); + new DisableHostnameVerification()); api.post(); } finally { server.shutdown(); @@ -465,7 +469,7 @@ static class DisableHostnameVerification { 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"); + OtherTestInterface i4 = Feign.create(OtherTestInterface.class, "http://localhost:8080", new TestInterface.Module()); assertTrue(i1.equals(i1)); assertTrue(i1.equals(i2)); diff --git a/core/src/test/java/feign/LoggerTest.java b/core/src/test/java/feign/LoggerTest.java index 7412377223..3a3fa41def 100644 --- a/core/src/test/java/feign/LoggerTest.java +++ b/core/src/test/java/feign/LoggerTest.java @@ -19,6 +19,7 @@ import com.google.mockwebserver.MockResponse; import com.google.mockwebserver.MockWebServer; import dagger.Provides; +import feign.codec.Decoder; import feign.codec.Encoder; import org.testng.annotations.BeforeMethod; import org.testng.annotations.DataProvider; @@ -122,7 +123,7 @@ public void levelEmits(final Logger.Level logLevel, List expectedMessage } } - static @dagger.Module(overrides = true, library = true) class DefaultModule { + static @dagger.Module(injects = Feign.class, overrides = true, addsTo = Feign.Defaults.class) class DefaultModule { final Logger logger; final Logger.Level logLevel; @@ -131,12 +132,12 @@ public void levelEmits(final Logger.Level logLevel, List expectedMessage this.logLevel = logLevel; } + @Provides Decoder defaultDecoder() { + return new Decoder.Default(); + } + @Provides Encoder defaultEncoder() { - return new Encoder() { - @Override public void encode(Object object, RequestTemplate template) { - template.body(object.toString()); - } - }; + return new Encoder.Default(); } @Provides @Singleton Logger logger() { diff --git a/gson/src/main/java/feign/gson/GsonModule.java b/gson/src/main/java/feign/gson/GsonModule.java index 16fad54856..cd3a031030 100644 --- a/gson/src/main/java/feign/gson/GsonModule.java +++ b/gson/src/main/java/feign/gson/GsonModule.java @@ -26,6 +26,7 @@ import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import dagger.Provides; +import feign.Feign; import feign.RequestTemplate; import feign.Response; import feign.codec.Decoder; @@ -79,7 +80,7 @@ * } * */ -@dagger.Module(library = true) +@dagger.Module(injects = Feign.class, addsTo = Feign.Defaults.class) public final class GsonModule { @Provides Encoder encoder(GsonCodec codec) { diff --git a/gson/src/test/java/feign/gson/GsonModuleTest.java b/gson/src/test/java/feign/gson/GsonModuleTest.java index 0ecd61e59a..86f9920b13 100644 --- a/gson/src/test/java/feign/gson/GsonModuleTest.java +++ b/gson/src/test/java/feign/gson/GsonModuleTest.java @@ -42,7 +42,7 @@ @Test public class GsonModuleTest { - @Module(includes = GsonModule.class, library = true, injects = EncoderAndDecoderBindings.class) + @Module(includes = GsonModule.class, injects = EncoderAndDecoderBindings.class) static class EncoderAndDecoderBindings { @Inject Encoder encoder; @Inject Decoder decoder; @@ -56,7 +56,7 @@ static class EncoderAndDecoderBindings { assertEquals(bindings.decoder.getClass(), GsonModule.GsonCodec.class); } - @Module(includes = GsonModule.class, library = true, injects = EncoderBindings.class) + @Module(includes = GsonModule.class, injects = EncoderBindings.class) static class EncoderBindings { @Inject Encoder encoder; } @@ -115,7 +115,7 @@ static class Zone extends LinkedHashMap { private static final long serialVersionUID = 1L; } - @Module(includes = GsonModule.class, library = true, injects = DecoderBindings.class) + @Module(includes = GsonModule.class, injects = DecoderBindings.class) static class DecoderBindings { @Inject Decoder decoder; } @@ -144,7 +144,7 @@ static class DecoderBindings { + " }\n"// + "]\n"; - @Module(includes = GsonModule.class, library = true, injects = CustomTypeAdapter.class) + @Module(includes = GsonModule.class, injects = CustomTypeAdapter.class) static class CustomTypeAdapter { @Provides(type = Provides.Type.SET) TypeAdapter upperZone() { return new TypeAdapter() { diff --git a/gson/src/test/java/feign/gson/examples/GitHubExample.java b/gson/src/test/java/feign/gson/examples/GitHubExample.java new file mode 100644 index 0000000000..66fa719496 --- /dev/null +++ b/gson/src/test/java/feign/gson/examples/GitHubExample.java @@ -0,0 +1,49 @@ +/* + * 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.gson.examples; + +import feign.Feign; +import feign.RequestLine; +import feign.gson.GsonModule; + +import javax.inject.Named; +import java.util.List; + +/** + * adapted from {@code com.example.retrofit.GitHubClient} + */ +public class GitHubExample { + + interface GitHub { + @RequestLine("GET /repos/{owner}/{repo}/contributors") + List contributors(@Named("owner") String owner, @Named("repo") String repo); + } + + 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 contributors = github.contributors("netflix", "feign"); + for (Contributor contributor : contributors) { + System.out.println(contributor.login + " (" + contributor.contributions + ")"); + } + } +} diff --git a/ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java b/ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java index 29e834e0d5..befef3c7a9 100644 --- a/ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java +++ b/ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java @@ -51,7 +51,7 @@ public void loadBalancingDefaultPolicyRoundRobin() throws IOException, Interrupt try { LoadBalancingTarget target = LoadBalancingTarget.create(TestInterface.class, "http://" + name); - TestInterface api = Feign.create(target); + TestInterface api = Feign.builder().target(target); api.post(); api.post(); diff --git a/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java b/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java index 2f49d56959..d16a738ce6 100644 --- a/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java +++ b/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java @@ -17,15 +17,16 @@ import com.google.mockwebserver.MockResponse; import com.google.mockwebserver.MockWebServer; - +import dagger.Provides; +import feign.Feign; +import feign.RequestLine; +import feign.codec.Decoder; +import feign.codec.Encoder; import org.testng.annotations.Test; import java.io.IOException; import java.net.URL; -import feign.Feign; -import feign.RequestLine; - import static com.netflix.config.ConfigurationManager.getConfigInstance; import static org.testng.Assert.assertEquals; @@ -33,6 +34,17 @@ public class RibbonClientTest { interface TestInterface { @RequestLine("POST /") void post(); + + @dagger.Module(injects = Feign.class, addsTo = Feign.Defaults.class) + static class Module { + @Provides Decoder defaultDecoder() { + return new Decoder.Default(); + } + + @Provides Encoder defaultEncoder() { + return new Encoder.Default(); + } + } } @Test @@ -51,7 +63,7 @@ public void loadBalancingDefaultPolicyRoundRobin() throws IOException, Interrupt try { - TestInterface api = Feign.create(TestInterface.class, "http://" + client, new RibbonModule()); + TestInterface api = Feign.create(TestInterface.class, "http://" + client, new TestInterface.Module(), new RibbonModule()); api.post(); api.post(); From 2597d3baf325029d15c21408f5a7d56807bd1e7a Mon Sep 17 00:00:00 2001 From: adriancole Date: Mon, 16 Sep 2013 10:22:01 +0200 Subject: [PATCH 106/179] Moved SaxDecoder into feign-sax dependency. --- CHANGES.md | 5 ++-- README.md | 12 ++++++---- build.gradle | 14 +++++++++++ sax/README.md | 24 +++++++++++++++++++ .../src/main/java/feign/sax}/SAXDecoder.java | 6 +++-- .../test/java/feign/sax}/SAXDecoderTest.java | 14 ++++++++++- .../sax}/examples/AWSSignatureVersion4.java | 2 +- .../java/feign/sax}/examples/IAMExample.java | 15 +++++++----- settings.gradle | 2 +- 9 files changed, 77 insertions(+), 17 deletions(-) create mode 100644 sax/README.md rename {core/src/main/java/feign/codec => sax/src/main/java/feign/sax}/SAXDecoder.java (96%) rename {core/src/test/java/feign/codec => sax/src/test/java/feign/sax}/SAXDecoderTest.java (88%) rename {core/src/test/java/feign => sax/src/test/java/feign/sax}/examples/AWSSignatureVersion4.java (99%) rename {core/src/test/java/feign => sax/src/test/java/feign/sax}/examples/IAMExample.java (90%) diff --git a/CHANGES.md b/CHANGES.md index c31e9f4973..af012f3a18 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,9 +1,10 @@ ### Version 5.0 * Remove support for Observable methods. -* SaxDecoder now decodes multiple types. -* Remove pattern decoders in favor of SaxDecoder. * Use single non-generic Decoder/Encoder instead of sets of type-specific Decoders/Encoders. * Decoders/Encoders are now more flexible, having access to the Response/RequestTemplate respectively. +* Moved SaxDecoder into `feign-sax` dependency. + * SaxDecoder now decodes multiple types. + * Remove pattern decoders in favor of SaxDecoder. * Added Feign.Builder to simplify client customizations without using Dagger. * Gson type adapters can be registered as Dagger set bindings. * `Feign.create(...)` now requires specifying an encoder and decoder. diff --git a/README.md b/README.md index 57d0c4754c..3541de16af 100644 --- a/README.md +++ b/README.md @@ -78,20 +78,24 @@ For example, the following pattern might decorate each request with the current CloudDNS cloudDNS = Feign.builder().target(new CloudIdentityTarget(user, apiKey)); ``` -You can find [several examples](https://github.com/Netflix/feign/tree/master/feign-core/src/test/java/feign/examples) in the test tree. Do take time to look at them, as seeing is believing! +You can find [several examples](https://github.com/Netflix/feign/tree/master/core/src/test/java/feign/examples) in the test tree. Do take time to look at them, as seeing is believing! ### Integrations Feign intends to work well within Netflix and other Open Source communities. Modules are welcome to integrate with your favorite projects! + ### Gson -[GsonModule](https://github.com/Netflix/feign/tree/master/feign-gson) adds default encoders and decoders so you get get started with a JSON api. +[GsonModule](https://github.com/Netflix/feign/tree/master/gson) adds default encoders and decoders so you get get started with a JSON api. Integration requires you pass `new GsonModule()` to `Feign.create()`, or add it to your graph with Dagger: ```java GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule()); ``` +### Sax +[SaxDecoder](https://github.com/Netflix/feign/tree/master/sax) allows you to decode XML in a way that is compatible with normal JVM and also Android environments. + ### JAX-RS -[JAXRSModule](https://github.com/Netflix/feign/tree/master/feign-jaxrs) overrides annotation processing to instead use standard ones supplied by the JAX-RS specification. This is currently targeted at the 1.1 spec. +[JAXRSModule](https://github.com/Netflix/feign/tree/master/jaxrs) overrides annotation processing to instead use standard ones supplied by the JAX-RS specification. This is currently targeted at the 1.1 spec. Here's the example above re-written to use JAX-RS: ```java @@ -101,7 +105,7 @@ interface GitHub { } ``` ### Ribbon -[RibbonModule](https://github.com/Netflix/feign/tree/master/feign-ribbon) overrides URL resolution of Feign's client, adding smart routing and resiliency capabilities provided by [Ribbon](https://github.com/Netflix/ribbon). +[RibbonModule](https://github.com/Netflix/feign/tree/master/ribbon) overrides URL resolution of Feign's client, adding smart routing and resiliency capabilities provided by [Ribbon](https://github.com/Netflix/ribbon). Integration requires you to pass your ribbon client name as the host part of the url, for example `myAppProd`. ```java diff --git a/build.gradle b/build.gradle index 7168bffff1..8a30186f8a 100644 --- a/build.gradle +++ b/build.gradle @@ -45,6 +45,20 @@ project(':feign-core') { } } +project(':feign-sax') { + apply plugin: 'java' + + test { + useTestNG() + } + + dependencies { + compile project(':feign-core') + testCompile 'com.google.guava:guava:14.0.1' + testCompile 'org.testng:testng:6.8.5' + } +} + project(':feign-gson') { apply plugin: 'java' diff --git a/sax/README.md b/sax/README.md new file mode 100644 index 0000000000..e522f6f281 --- /dev/null +++ b/sax/README.md @@ -0,0 +1,24 @@ +Sax Decoder +=================== + +This module adds support for decoding xml via SAX. + +Add this to your object graph like so: + +```java +IAM iam = Feign.create(IAM.class, "https://iam.amazonaws.com", new DecodeWithSax()); + +--snip-- +@Module(addsTo = Feign.Defaults.class) +static class DecodeWithSax { + @Provides Decoder saxDecoder(Provider userIdHandler) { + return SAXDecoder.builder() // + .addContentHandler(userIdHandler) // + .build(); + } + + @Provides Encoder defaultEncoder() { + return new Encoder.Default(); + } +} +``` diff --git a/core/src/main/java/feign/codec/SAXDecoder.java b/sax/src/main/java/feign/sax/SAXDecoder.java similarity index 96% rename from core/src/main/java/feign/codec/SAXDecoder.java rename to sax/src/main/java/feign/sax/SAXDecoder.java index 46cf17508f..1d4672492b 100644 --- a/core/src/main/java/feign/codec/SAXDecoder.java +++ b/sax/src/main/java/feign/sax/SAXDecoder.java @@ -13,9 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package feign.codec; +package feign.sax; import feign.Response; +import feign.codec.Decoder; +import feign.codec.DecodeException; import org.xml.sax.ContentHandler; import org.xml.sax.InputSource; import org.xml.sax.SAXException; @@ -90,7 +92,7 @@ private SAXDecoder(Map>> ha @Override public Object decode(Response response, Type type) throws IOException, DecodeException { - if (response.body() == null) { + if (void.class.equals(type) || response.body() == null) { return null; } Provider> handlerProvider = handlerProviders.get(type); diff --git a/core/src/test/java/feign/codec/SAXDecoderTest.java b/sax/src/test/java/feign/sax/SAXDecoderTest.java similarity index 88% rename from core/src/test/java/feign/codec/SAXDecoderTest.java rename to sax/src/test/java/feign/sax/SAXDecoderTest.java index f434302bab..0db5302335 100644 --- a/core/src/test/java/feign/codec/SAXDecoderTest.java +++ b/sax/src/test/java/feign/sax/SAXDecoderTest.java @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package feign.codec; +package feign.sax; import dagger.ObjectGraph; import dagger.Provides; +import feign.codec.Decoder; +import feign.codec.DecodeException; import feign.Response; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; @@ -135,4 +137,14 @@ public void characters(char ch[], int start, int length) { currentText.append(ch, start, length); } } + + @Test public void voidDecodesToNull() throws Exception { + Response response = Response.create(200, "OK", Collections.>emptyMap(), statusFailed); + assertEquals(decoder.decode(response, void.class), null); + } + + @Test public void nullBodyDecodesToNull() throws Exception { + Response response = Response.create(204, "OK", Collections.>emptyMap(), null); + assertEquals(decoder.decode(response, String.class), null); + } } diff --git a/core/src/test/java/feign/examples/AWSSignatureVersion4.java b/sax/src/test/java/feign/sax/examples/AWSSignatureVersion4.java similarity index 99% rename from core/src/test/java/feign/examples/AWSSignatureVersion4.java rename to sax/src/test/java/feign/sax/examples/AWSSignatureVersion4.java index 40c83eda84..d9751675ff 100644 --- a/core/src/test/java/feign/examples/AWSSignatureVersion4.java +++ b/sax/src/test/java/feign/sax/examples/AWSSignatureVersion4.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package feign.examples; +package feign.sax.examples; import com.google.common.base.Function; import com.google.common.base.Joiner; diff --git a/core/src/test/java/feign/examples/IAMExample.java b/sax/src/test/java/feign/sax/examples/IAMExample.java similarity index 90% rename from core/src/test/java/feign/examples/IAMExample.java rename to sax/src/test/java/feign/sax/examples/IAMExample.java index 540ca0faf7..2bdb583ac7 100644 --- a/core/src/test/java/feign/examples/IAMExample.java +++ b/sax/src/test/java/feign/sax/examples/IAMExample.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package feign.examples; +package feign.sax.examples; import dagger.Module; import dagger.Provides; @@ -23,14 +23,13 @@ import feign.RequestTemplate; import feign.Target; import feign.codec.Decoder; -import feign.codec.SAXDecoder; +import feign.codec.Encoder; +import feign.sax.SAXDecoder; import org.xml.sax.helpers.DefaultHandler; import javax.inject.Inject; import javax.inject.Provider; -import static dagger.Provides.Type.SET; - public class IAMExample { interface IAM { @@ -66,13 +65,17 @@ private IAMTarget(String accessKey, String secretKey) { } } - @Module(library = true) + @Module(addsTo = Feign.Defaults.class) static class DecodeWithSax { - @Provides(type = SET) Decoder saxDecoder(Provider userIdHandler) { + @Provides Decoder saxDecoder(Provider userIdHandler) { return SAXDecoder.builder() // .addContentHandler(userIdHandler) // .build(); } + + @Provides Encoder defaultEncoder() { + return new Encoder.Default(); + } } static class UserIdHandler extends DefaultHandler implements diff --git a/settings.gradle b/settings.gradle index a7bf699763..b7b41a0482 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,5 @@ rootProject.name='feign' -include 'core', 'gson', 'jaxrs', 'ribbon', 'example-github', 'example-wikipedia' +include 'core', 'sax', 'gson', 'jaxrs', 'ribbon', 'example-github', 'example-wikipedia' rootProject.children.each { childProject -> childProject.name = 'feign-' + childProject.name From 50e3cf54b7b2443a92c2ecbd42b41c065f89607a Mon Sep 17 00:00:00 2001 From: adriancole Date: Mon, 16 Sep 2013 14:39:32 -0700 Subject: [PATCH 107/179] ensure gson does not attempt to decode void --- gson/src/main/java/feign/gson/GsonModule.java | 2 +- .../src/test/java/feign/gson/GsonModuleTest.java | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/gson/src/main/java/feign/gson/GsonModule.java b/gson/src/main/java/feign/gson/GsonModule.java index cd3a031030..0c33f75085 100644 --- a/gson/src/main/java/feign/gson/GsonModule.java +++ b/gson/src/main/java/feign/gson/GsonModule.java @@ -103,7 +103,7 @@ static class GsonCodec implements Encoder, Decoder { } @Override public Object decode(Response response, Type type) throws IOException { - if (response.body() == null) { + if (void.class.equals(type) || response.body() == null) { return null; } Reader reader = response.body().asReader(); diff --git a/gson/src/test/java/feign/gson/GsonModuleTest.java b/gson/src/test/java/feign/gson/GsonModuleTest.java index 86f9920b13..8a15db95cc 100644 --- a/gson/src/test/java/feign/gson/GsonModuleTest.java +++ b/gson/src/test/java/feign/gson/GsonModuleTest.java @@ -133,6 +133,22 @@ static class DecoderBindings { }.getType()), zones); } + @Test public void voidDecodesToNull() throws Exception { + DecoderBindings bindings = new DecoderBindings(); + ObjectGraph.create(bindings).inject(bindings); + + Response response = Response.create(200, "OK", Collections.>emptyMap(), zonesJson); + assertEquals(bindings.decoder.decode(response, void.class), null); + } + + @Test public void nullBodyDecodesToNull() throws Exception { + DecoderBindings bindings = new DecoderBindings(); + ObjectGraph.create(bindings).inject(bindings); + + Response response = Response.create(204, "OK", Collections.>emptyMap(), null); + assertEquals(bindings.decoder.decode(response, String.class), null); + } + private String zonesJson = ""// + "[\n"// + " {\n"// From b389ef03ba522eb33ee562d656101f7ddd1563b7 Mon Sep 17 00:00:00 2001 From: adriancole Date: Tue, 17 Sep 2013 11:31:59 -0700 Subject: [PATCH 108/179] support Feign.builder() w/ SAX decoder --- README.md | 9 +++ sax/README.md | 20 ++--- sax/src/main/java/feign/sax/SAXDecoder.java | 79 +++++++++++++++---- .../test/java/feign/sax/SAXDecoderTest.java | 10 +-- .../java/feign/sax/examples/IAMExample.java | 29 +------ 5 files changed, 87 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 3541de16af..c8dfcd3c33 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,15 @@ GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonMod ### Sax [SaxDecoder](https://github.com/Netflix/feign/tree/master/sax) allows you to decode XML in a way that is compatible with normal JVM and also Android environments. +Here's an example of how to configure Sax response parsing: +```java +api = Feign.builder() + .decoder(SAXDecoder.builder() + .registerContentHandler(UserIdHandler.class) + .build()) + .target(Api.class, "https://apihost"); +``` + ### JAX-RS [JAXRSModule](https://github.com/Netflix/feign/tree/master/jaxrs) overrides annotation processing to instead use standard ones supplied by the JAX-RS specification. This is currently targeted at the 1.1 spec. diff --git a/sax/README.md b/sax/README.md index e522f6f281..1c901ed653 100644 --- a/sax/README.md +++ b/sax/README.md @@ -6,19 +6,9 @@ This module adds support for decoding xml via SAX. Add this to your object graph like so: ```java -IAM iam = Feign.create(IAM.class, "https://iam.amazonaws.com", new DecodeWithSax()); - ---snip-- -@Module(addsTo = Feign.Defaults.class) -static class DecodeWithSax { - @Provides Decoder saxDecoder(Provider userIdHandler) { - return SAXDecoder.builder() // - .addContentHandler(userIdHandler) // - .build(); - } - - @Provides Encoder defaultEncoder() { - return new Encoder.Default(); - } -} +api = Feign.builder() + .decoder(SAXDecoder.builder() + .registerContentHandler(UserIdHandler.class) + .build()) + .target(Api.class, "https://apihost"); ``` diff --git a/sax/src/main/java/feign/sax/SAXDecoder.java b/sax/src/main/java/feign/sax/SAXDecoder.java index 1d4672492b..944f325579 100644 --- a/sax/src/main/java/feign/sax/SAXDecoder.java +++ b/sax/src/main/java/feign/sax/SAXDecoder.java @@ -16,8 +16,8 @@ package feign.sax; import feign.Response; -import feign.codec.Decoder; import feign.codec.DecodeException; +import feign.codec.Decoder; import org.xml.sax.ContentHandler; import org.xml.sax.InputSource; import org.xml.sax.SAXException; @@ -27,6 +27,7 @@ import javax.inject.Provider; import java.io.IOException; import java.io.Reader; +import java.lang.reflect.Constructor; import java.lang.reflect.Type; import java.util.LinkedHashMap; import java.util.Map; @@ -37,17 +38,28 @@ import static feign.Util.resolveLastTypeParameter; /** - * Decodes responses using SAX. Configure using the {@link SAXDecoder.Builder - * builder}. + * Decodes responses using SAX, which is supported both in normal JVM environments, as well Android. + *
+ *

Basic example with with Feign.Builder

+ *
+ *
+ * api = Feign.builder()
+ *            .decoder(SAXDecoder.builder()
+ *                               .registerContentHandler(ContentHandlerForFoo.class)
+ *                               .registerContentHandler(ContentHandlerForBar.class)
+ *                               .build())
+ *            .target(MyApi.class, "http://api");
+ * 
*

- * + *

Advanced example with Dagger

+ *
*
  * @Provides
  * Decoder saxDecoder(Provider<ContentHandlerForFoo> foo, //
  *         Provider<ContentHandlerForBar> bar) {
  *     return SAXDecoder.builder() //
- *             .addContentHandler(foo) //
- *             .addContentHandler(bar) //
+ *             .registerContentHandler(Foo.class, foo) //
+ *             .registerContentHandler(Bar.class, bar) //
  *             .build();
  * }
  * 
@@ -63,10 +75,48 @@ public static class Builder { private final Map>> handlerProviders = new LinkedHashMap>>(); - public Builder addContentHandler(Provider> handler) { - Type type = resolveLastTypeParameter(checkNotNull(handler, "handler").getClass(), Provider.class); - type = resolveLastTypeParameter(type, ContentHandlerWithResult.class); - this.handlerProviders.put(type, handler); + /** + * Will call {@link Constructor#newInstance(Object...)} on {@code handlerClass} for each content stream. + *

+ *

Note

+ *
+ * While this is costly vs {@code new}, it may not affect real performance due to the high cost of reading streams. + * + * @throws IllegalArgumentException if there's no no-arg constructor on {@code handlerClass}. + */ + public > Builder registerContentHandler(Class handlerClass) { + Type type = resolveLastTypeParameter(checkNotNull(handlerClass, "handlerClass"), ContentHandlerWithResult.class); + return registerContentHandler(type, new NewInstanceProvider(handlerClass)); + } + + private static class NewInstanceProvider> implements Provider { + private final Constructor ctor; + + private NewInstanceProvider(Class clazz) { + try { + this.ctor = clazz.getDeclaredConstructor(); + // allow private or package protected ctors + ctor.setAccessible(true); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException("ensure " + clazz + " has a no-args constructor", e); + } + } + + @Override public T get() { + try { + return ctor.newInstance(); + } catch (Exception e) { + throw new IllegalArgumentException("exception attempting to instantiate " + ctor, e); + } + } + } + + /** + * Will call {@link Provider#get()} on {@code handler} for each content stream. + * The {@code handler} is expected to have a generic parameter of {@code type}. + */ + public Builder registerContentHandler(Type type, Provider> handler) { + this.handlerProviders.put(checkNotNull(type, "type"), checkNotNull(handler, "handler")); return this; } @@ -75,11 +125,12 @@ public SAXDecoder build() { } } - /* Implementations are not intended to be shared across requests. */ + /** + * Implementations are not intended to be shared across requests. + */ public interface ContentHandlerWithResult extends ContentHandler { - /* - * expected to be set following a call to {@link - * XMLReader#parse(InputSource)} + /** + * expected to be set following a call to {@link XMLReader#parse(InputSource)} */ T result(); } diff --git a/sax/src/test/java/feign/sax/SAXDecoderTest.java b/sax/src/test/java/feign/sax/SAXDecoderTest.java index 0db5302335..b01af77cc9 100644 --- a/sax/src/test/java/feign/sax/SAXDecoderTest.java +++ b/sax/src/test/java/feign/sax/SAXDecoderTest.java @@ -17,9 +17,8 @@ import dagger.ObjectGraph; import dagger.Provides; -import feign.codec.Decoder; -import feign.codec.DecodeException; import feign.Response; +import feign.codec.Decoder; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.xml.sax.helpers.DefaultHandler; @@ -39,11 +38,10 @@ public class SAXDecoderTest { @dagger.Module(injects = SAXDecoderTest.class) static class Module { - @Provides Decoder saxDecoder(Provider networkStatus, // - Provider networkStatusAsString) { + @Provides Decoder saxDecoder(Provider networkStatus) { return SAXDecoder.builder() // - .addContentHandler(networkStatus) // - .addContentHandler(networkStatusAsString) // + .registerContentHandler(NetworkStatus.class, networkStatus) // + .registerContentHandler(NetworkStatusStringHandler.class) // .build(); } } diff --git a/sax/src/test/java/feign/sax/examples/IAMExample.java b/sax/src/test/java/feign/sax/examples/IAMExample.java index 2bdb583ac7..e00b7be493 100644 --- a/sax/src/test/java/feign/sax/examples/IAMExample.java +++ b/sax/src/test/java/feign/sax/examples/IAMExample.java @@ -15,21 +15,14 @@ */ package feign.sax.examples; -import dagger.Module; -import dagger.Provides; import feign.Feign; import feign.Request; import feign.RequestLine; import feign.RequestTemplate; import feign.Target; -import feign.codec.Decoder; -import feign.codec.Encoder; import feign.sax.SAXDecoder; import org.xml.sax.helpers.DefaultHandler; -import javax.inject.Inject; -import javax.inject.Provider; - public class IAMExample { interface IAM { @@ -37,7 +30,9 @@ interface IAM { } public static void main(String... args) { - IAM iam = Feign.create(new IAMTarget(args[0], args[1]), new DecodeWithSax()); + IAM iam = Feign.builder()// + .decoder(SAXDecoder.builder().registerContentHandler(UserIdHandler.class).build())// + .target(new IAMTarget(args[0], args[1])); System.out.println(iam.userId()); } @@ -65,23 +60,7 @@ private IAMTarget(String accessKey, String secretKey) { } } - @Module(addsTo = Feign.Defaults.class) - static class DecodeWithSax { - @Provides Decoder saxDecoder(Provider userIdHandler) { - return SAXDecoder.builder() // - .addContentHandler(userIdHandler) // - .build(); - } - - @Provides Encoder defaultEncoder() { - return new Encoder.Default(); - } - } - - static class UserIdHandler extends DefaultHandler implements - SAXDecoder.ContentHandlerWithResult { - @Inject UserIdHandler() { - } + static class UserIdHandler extends DefaultHandler implements SAXDecoder.ContentHandlerWithResult { private StringBuilder currentText = new StringBuilder(); From f81aed25e29744c272339c72e8159e54e8b7fe99 Mon Sep 17 00:00:00 2001 From: adriancole Date: Tue, 17 Sep 2013 13:48:32 -0700 Subject: [PATCH 109/179] removed experimental disclaimer --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index c8dfcd3c33..e0a8db5fb6 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,6 @@ # Feign makes writing java http clients easier Feign is a java to http client binder inspired by [Dagger](https://github.com/square/dagger), [Retrofit](https://github.com/square/retrofit), [JAXRS-2.0](https://jax-rs-spec.java.net/nonav/2.0/apidocs/index.html), and [WebSocket](http://www.oracle.com/technetwork/articles/java/jsr356-1937161.html). Feign's first goal was reducing the complexity of binding [Denominator](https://github.com/Netflix/Denominator) uniformly to http apis regardless of [restfulness](http://www.slideshare.net/adrianfcole/99problems). -## Disclaimer -Feign is experimental and [being simplified further](https://github.com/Netflix/feign/issues/53) in version 5. Particularly, this will impact how encoders and encoders are declared, and remove support for observable methods. - ### Why Feign and not X? You can use tools like Jersey and CXF to write java clients for ReST or SOAP services. You can write your own code on top of http transport libraries like Apache HC. Feign aims to connect your code to http apis with minimal overhead and code. Via customizable decoders and error handling, you should be able to write to any text-based http api. From ec2f1ec2730486b654e35c8d68e03e2a2097e775 Mon Sep 17 00:00:00 2001 From: adriancole Date: Tue, 17 Sep 2013 13:50:04 -0700 Subject: [PATCH 110/179] 6.0.0-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 11d8403777..a2f0b2797c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=5.0.0-SNAPSHOT +version=6.0.0-SNAPSHOT From d4be9153159c42dbc153fee1d9ecbbe1b2d85a2b Mon Sep 17 00:00:00 2001 From: adriancole Date: Tue, 17 Sep 2013 14:19:14 -0700 Subject: [PATCH 111/179] update examples to feign 5.x --- .../java/feign/examples/GitHubExample.java | 59 ++++--------------- example-github/build.gradle | 4 +- .../feign/example/github/GitHubExample.java | 47 +-------------- example-wikipedia/build.gradle | 4 +- ...ponseDecoder.java => ResponseAdapter.java} | 15 +++-- .../example/wikipedia/WikipediaExample.java | 10 ++-- 6 files changed, 31 insertions(+), 108 deletions(-) rename example-wikipedia/src/main/java/feign/example/wikipedia/{ResponseDecoder.java => ResponseAdapter.java} (85%) diff --git a/core/src/test/java/feign/examples/GitHubExample.java b/core/src/test/java/feign/examples/GitHubExample.java index 02223ad564..c52308d52f 100644 --- a/core/src/test/java/feign/examples/GitHubExample.java +++ b/core/src/test/java/feign/examples/GitHubExample.java @@ -17,18 +17,13 @@ import com.google.gson.Gson; import com.google.gson.JsonIOException; -import com.google.gson.stream.JsonReader; -import dagger.Module; -import dagger.Provides; import feign.Feign; import feign.Logger; import feign.RequestLine; import feign.Response; import feign.codec.Decoder; -import javax.inject.Inject; import javax.inject.Named; -import javax.inject.Singleton; import java.io.IOException; import java.io.Reader; import java.lang.reflect.Type; @@ -51,8 +46,12 @@ static class Contributor { int contributions; } - public static void main(String... args) throws InterruptedException { - GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GitHubModule()); + public static void main(String... args) { + GitHub github = Feign.builder() + .logger(new Logger.ErrorLogger()) + .logLevel(Logger.Level.BASIC) + .decoder(new GsonDecoder()) + .target(GitHub.class, "https://api.github.com"); System.out.println("Let's fetch and print a list of the contributors to this library."); List contributors = github.contributors("netflix", "feign"); @@ -61,60 +60,26 @@ public static void main(String... args) throws InterruptedException { } } - @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(); - } - } - /** - * Here's how it looks to wire json codecs. Note, that you can always instead use {@code feign-gson}! + * Here's how it looks to write a decoder. Note: you can instead use {@code feign-gson}! */ - @Module(library = true) - static class GsonModule { - - @Provides @Singleton Gson gson() { - return new Gson(); - } - - @Provides Decoder decoder(GsonDecoder gsonDecoder) { - return gsonDecoder; - } - } - static class GsonDecoder implements Decoder { - private final Gson gson; - - @Inject GsonDecoder(Gson gson) { - this.gson = gson; - } + private final Gson gson = new Gson(); @Override public Object decode(Response response, Type type) throws IOException { - if (response.body() == null) { + if (void.class == type || response.body() == null) { return null; } Reader reader = response.body().asReader(); try { - return fromJson(new JsonReader(reader), type); - } finally { - ensureClosed(reader); - } - } - - private Object fromJson(JsonReader jsonReader, Type type) throws IOException { - try { - return gson.fromJson(jsonReader, type); + return gson.fromJson(reader, type); } catch (JsonIOException e) { if (e.getCause() != null && e.getCause() instanceof IOException) { throw IOException.class.cast(e.getCause()); } throw e; + } finally { + ensureClosed(reader); } } } diff --git a/example-github/build.gradle b/example-github/build.gradle index 126b8632df..24049dc0c6 100644 --- a/example-github/build.gradle +++ b/example-github/build.gradle @@ -1,8 +1,8 @@ apply plugin: 'java' dependencies { - compile 'com.netflix.feign:feign-core:4.3.0' - compile 'com.netflix.feign:feign-gson:4.3.0' + compile 'com.netflix.feign:feign-core:5.0.0' + compile 'com.netflix.feign:feign-gson:5.0.0' provided 'com.squareup.dagger:dagger-compiler:1.1.0' } diff --git a/example-github/src/main/java/feign/example/github/GitHubExample.java b/example-github/src/main/java/feign/example/github/GitHubExample.java index 6f8977913b..900bfc18b8 100644 --- a/example-github/src/main/java/feign/example/github/GitHubExample.java +++ b/example-github/src/main/java/feign/example/github/GitHubExample.java @@ -19,14 +19,11 @@ import dagger.Provides; import feign.Feign; import feign.Logger; -import feign.Observable; -import feign.Observer; 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} @@ -36,9 +33,6 @@ public class GitHubExample { interface GitHub { @RequestLine("GET /repos/{owner}/{repo}/contributors") List contributors(@Named("owner") String owner, @Named("repo") String repo); - - @RequestLine("GET /repos/{owner}/{repo}/contributors") - Observable observable(@Named("owner") String owner, @Named("repo") String repo); } static class Contributor { @@ -54,48 +48,9 @@ public static void main(String... args) throws InterruptedException { for (Contributor contributor : contributors) { System.out.println(contributor.login + " (" + contributor.contributions + ")"); } - - System.out.println("Let's treat our contributors as an observable."); - Observable observable = github.observable("netflix", "feign"); - - CountDownLatch latch = new CountDownLatch(2); - - System.out.println("Let's add 2 subscribers."); - observable.subscribe(new ContributorObserver(latch)); - observable.subscribe(new ContributorObserver(latch)); - - // wait for the task to complete. - latch.await(); - - System.exit(0); - } - - static class ContributorObserver implements Observer { - - private final CountDownLatch latch; - public int count; - - public ContributorObserver(CountDownLatch latch) { - this.latch = latch; - } - - // 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) + @Module(overrides = true, library = true, includes = GsonModule.class) static class LogToStderr { @Provides Logger.Level loggingLevel() { diff --git a/example-wikipedia/build.gradle b/example-wikipedia/build.gradle index 816eda6481..73c6b99624 100644 --- a/example-wikipedia/build.gradle +++ b/example-wikipedia/build.gradle @@ -1,8 +1,8 @@ apply plugin: 'java' dependencies { - compile 'com.netflix.feign:feign-core:4.3.0' - compile 'com.netflix.feign:feign-gson:4.3.0' + compile 'com.netflix.feign:feign-core:5.0.0' + compile 'com.netflix.feign:feign-gson:5.0.0' provided 'com.squareup.dagger:dagger-compiler:1.1.0' } diff --git a/example-wikipedia/src/main/java/feign/example/wikipedia/ResponseDecoder.java b/example-wikipedia/src/main/java/feign/example/wikipedia/ResponseAdapter.java similarity index 85% rename from example-wikipedia/src/main/java/feign/example/wikipedia/ResponseDecoder.java rename to example-wikipedia/src/main/java/feign/example/wikipedia/ResponseAdapter.java index 9cb54bba9a..e202cc109b 100644 --- a/example-wikipedia/src/main/java/feign/example/wikipedia/ResponseDecoder.java +++ b/example-wikipedia/src/main/java/feign/example/wikipedia/ResponseAdapter.java @@ -1,13 +1,12 @@ package feign.example.wikipedia; +import com.google.gson.TypeAdapter; import com.google.gson.stream.JsonReader; -import feign.codec.Decoder; +import com.google.gson.stream.JsonWriter; import java.io.IOException; -import java.io.Reader; -import java.lang.reflect.Type; -abstract class ResponseDecoder implements Decoder.TextStream> { +abstract class ResponseAdapter extends TypeAdapter> { /** * name of the key inside the {@code query} dict which holds the elements desired. ex. {@code pages}. @@ -35,9 +34,8 @@ abstract class ResponseDecoder implements Decoder.TextStream decode(Reader ireader, Type type) throws IOException { + public WikipediaExample.Response read(JsonReader reader) throws IOException { WikipediaExample.Response pages = new WikipediaExample.Response(); - JsonReader reader = new JsonReader(ireader); reader.beginObject(); while (reader.hasNext()) { String nextName = reader.nextName(); @@ -84,4 +82,9 @@ public WikipediaExample.Response decode(Reader ireader, Type type) throws IOE reader.close(); return pages; } + + @Override + public void write(JsonWriter out, WikipediaExample.Response response) throws IOException { + throw new UnsupportedOperationException(); + } } diff --git a/example-wikipedia/src/main/java/feign/example/wikipedia/WikipediaExample.java b/example-wikipedia/src/main/java/feign/example/wikipedia/WikipediaExample.java index 90ee691636..feb5712174 100644 --- a/example-wikipedia/src/main/java/feign/example/wikipedia/WikipediaExample.java +++ b/example-wikipedia/src/main/java/feign/example/wikipedia/WikipediaExample.java @@ -15,13 +15,13 @@ */ package feign.example.wikipedia; +import com.google.gson.TypeAdapter; 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; @@ -101,14 +101,14 @@ public void remove() { }; } - @Module(library = true, includes = GsonModule.class) + @Module(includes = GsonModule.class) static class WikipediaDecoder { /** - * add to the set of Decoders one that handles {@code Response}. + * registers a gson {@link TypeAdapter} for {@code Response}. */ - @Provides(type = SET) Decoder pagesDecoder() { - return new ResponseDecoder() { + @Provides(type = SET) TypeAdapter pagesAdapter() { + return new ResponseAdapter() { @Override protected String query() { From 73aaf8811328f50c9405f79112bc8b3b73c3d3ae Mon Sep 17 00:00:00 2001 From: adriancole Date: Tue, 17 Sep 2013 16:08:18 -0700 Subject: [PATCH 112/179] Decoder.decode() is no longer called for Response or void types. --- CHANGES.md | 3 +++ core/src/main/java/feign/MethodHandler.java | 12 ++++++++- core/src/main/java/feign/codec/Decoder.java | 27 +++++-------------- core/src/test/java/feign/FeignTest.java | 20 ++++++++++++++ .../java/feign/codec/DefaultDecoderTest.java | 15 ----------- gson/src/main/java/feign/gson/GsonModule.java | 2 +- .../test/java/feign/gson/GsonModuleTest.java | 8 ------ sax/src/main/java/feign/sax/SAXDecoder.java | 2 +- .../test/java/feign/sax/SAXDecoderTest.java | 5 ---- 9 files changed, 43 insertions(+), 51 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index af012f3a18..75cb7e0f28 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,6 @@ +### Version 5.0.1 +* `Decoder.decode()` is no longer called for `Response` or `void` types. + ### Version 5.0 * Remove support for Observable methods. * Use single non-generic Decoder/Encoder instead of sets of type-specific Decoders/Encoders. diff --git a/core/src/main/java/feign/MethodHandler.java b/core/src/main/java/feign/MethodHandler.java index 7b21938545..b44a5fcff3 100644 --- a/core/src/main/java/feign/MethodHandler.java +++ b/core/src/main/java/feign/MethodHandler.java @@ -143,7 +143,17 @@ Object executeAndDecode(RequestTemplate template) throws Throwable { response = logger.logAndRebufferResponse(metadata.configKey(), logLevel.get(), response, elapsedTime); } if (response.status() >= 200 && response.status() < 300) { - return decode(response); + if (Response.class == metadata.returnType()) { + if (response.body() == null) { + return response; + } + String bodyString = Util.toString(response.body().asReader()); + return Response.create(response.status(), response.reason(), response.headers(), bodyString); + } else if (void.class == metadata.returnType()) { + return null; + } else { + return decode(response); + } } else { throw errorDecoder.decode(metadata.configKey(), response); } diff --git a/core/src/main/java/feign/codec/Decoder.java b/core/src/main/java/feign/codec/Decoder.java index 54f078fc50..0c20a51389 100644 --- a/core/src/main/java/feign/codec/Decoder.java +++ b/core/src/main/java/feign/codec/Decoder.java @@ -26,20 +26,14 @@ /** * Decodes an HTTP response into a single object of the given {@code Type}. Invoked when - * {@link Response#status()} is in the 2xx range. Like - * {@code javax.websocket.Decoder}, except that the decode method is passed the - * generic type of the target. - * - *

+ * {@link Response#status()} is in the 2xx range and the return type is neither {@code void} nor {@code Response}. + *

+ *

* Example Implementation:
*

*

  * public class GsonDecoder implements Decoder {
- *   private final Gson gson;
- *
- *   public GsonDecoder(Gson gson) {
- *     this.gson = gson;
- *   }
+ *   private final Gson gson = new Gson();
  *
  *   @Override
  *   public Object decode(Response response, Type type) throws IOException {
@@ -62,7 +56,7 @@ public interface Decoder {
    * If you need to wrap exceptions, please do so via {@link DecodeException}.
    *
    * @param response the response to decode
-   * @param type  Target object type.
+   * @param type     Target object type.
    * @return instance of {@code type}
    * @throws IOException     will be propagated safely to the caller.
    * @throws DecodeException when decoding failed due to a checked exception besides IOException.
@@ -71,19 +65,12 @@ public interface Decoder {
   Object decode(Response response, Type type) throws IOException, DecodeException, FeignException;
 
   /**
-   * Default implementation of {@code Decoder} that supports {@code void}, {@code Response}, and {@code String}
-   * signatures.
+   * Default implementation of {@code Decoder} that supports {@code String} signatures.
    */
   public class Default implements Decoder {
     @Override
     public Object decode(Response response, Type type) throws IOException {
-      if (Response.class.equals(type)) {
-        String bodyString = null;
-        if (response.body() != null) {
-          bodyString = Util.toString(response.body().asReader());
-        }
-        return Response.create(response.status(), response.reason(), response.headers(), bodyString);
-      } else if (void.class.equals(type) || response.body() == null) {
+      if (response.body() == null) {
         return null;
       } else if (String.class.equals(type)) {
         return Util.toString(response.body().asReader());
diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java
index 873ac6258c..495efc4b92 100644
--- a/core/src/test/java/feign/FeignTest.java
+++ b/core/src/test/java/feign/FeignTest.java
@@ -55,6 +55,8 @@
 public class FeignTest {
 
   interface TestInterface {
+    @RequestLine("POST /") Response response();
+
     @RequestLine("POST /") String post();
 
     @RequestLine("POST /")
@@ -141,6 +143,24 @@ public void postTemplateParamsResolve() throws IOException, InterruptedException
     }
   }
 
+  @Test
+  public void responseCoercesToStringBody() 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());
+
+      Response response = api.response();
+      assertTrue(response.body().isRepeatable());
+      assertEquals(response.body().toString(), "foo");
+    } finally {
+      server.shutdown();
+    }
+  }
+
   @Test
   public void postFormParams() throws IOException, InterruptedException {
     final MockWebServer server = new MockWebServer();
diff --git a/core/src/test/java/feign/codec/DefaultDecoderTest.java b/core/src/test/java/feign/codec/DefaultDecoderTest.java
index 9442d760f0..08da68bb4b 100644
--- a/core/src/test/java/feign/codec/DefaultDecoderTest.java
+++ b/core/src/test/java/feign/codec/DefaultDecoderTest.java
@@ -32,21 +32,6 @@
 public class DefaultDecoderTest {
   private final Decoder decoder = new Decoder.Default();
 
-  @Test public void testDecodesToVoid() throws Exception {
-    assertEquals(decoder.decode(knownResponse(), void.class), null);
-  }
-
-  @Test public void testDecodesToResponse() throws Exception {
-    Response response = knownResponse();
-    Object decodedObject = decoder.decode(response, Response.class);
-    assertEquals(decodedObject.getClass(), Response.class);
-    Response decodedResponse = (Response) decodedObject;
-    assertEquals(decodedResponse.status(), response.status());
-    assertEquals(decodedResponse.reason(), response.reason());
-    assertEquals(decodedResponse.headers(), response.headers());
-    assertEquals(Util.toString(decodedResponse.body().asReader()), "response body");
-  }
-
   @Test public void testDecodesToString() throws Exception {
     Response response = knownResponse();
     Object decodedObject = decoder.decode(response, String.class);
diff --git a/gson/src/main/java/feign/gson/GsonModule.java b/gson/src/main/java/feign/gson/GsonModule.java
index 0c33f75085..cd3a031030 100644
--- a/gson/src/main/java/feign/gson/GsonModule.java
+++ b/gson/src/main/java/feign/gson/GsonModule.java
@@ -103,7 +103,7 @@ static class GsonCodec implements Encoder, Decoder {
     }
 
     @Override public Object decode(Response response, Type type) throws IOException {
-      if (void.class.equals(type) || response.body() == null) {
+      if (response.body() == null) {
         return null;
       }
       Reader reader = response.body().asReader();
diff --git a/gson/src/test/java/feign/gson/GsonModuleTest.java b/gson/src/test/java/feign/gson/GsonModuleTest.java
index 8a15db95cc..e8a23d76fd 100644
--- a/gson/src/test/java/feign/gson/GsonModuleTest.java
+++ b/gson/src/test/java/feign/gson/GsonModuleTest.java
@@ -133,14 +133,6 @@ static class DecoderBindings {
     }.getType()), zones);
   }
 
-  @Test public void voidDecodesToNull() throws Exception {
-    DecoderBindings bindings = new DecoderBindings();
-    ObjectGraph.create(bindings).inject(bindings);
-
-    Response response = Response.create(200, "OK", Collections.>emptyMap(), zonesJson);
-    assertEquals(bindings.decoder.decode(response, void.class), null);
-  }
-
   @Test public void nullBodyDecodesToNull() throws Exception {
     DecoderBindings bindings = new DecoderBindings();
     ObjectGraph.create(bindings).inject(bindings);
diff --git a/sax/src/main/java/feign/sax/SAXDecoder.java b/sax/src/main/java/feign/sax/SAXDecoder.java
index 944f325579..17981d734f 100644
--- a/sax/src/main/java/feign/sax/SAXDecoder.java
+++ b/sax/src/main/java/feign/sax/SAXDecoder.java
@@ -143,7 +143,7 @@ private SAXDecoder(Map>> ha
 
   @Override
   public Object decode(Response response, Type type) throws IOException, DecodeException {
-    if (void.class.equals(type) || response.body() == null) {
+    if (response.body() == null) {
       return null;
     }
     Provider> handlerProvider = handlerProviders.get(type);
diff --git a/sax/src/test/java/feign/sax/SAXDecoderTest.java b/sax/src/test/java/feign/sax/SAXDecoderTest.java
index b01af77cc9..d10fd4fe9a 100644
--- a/sax/src/test/java/feign/sax/SAXDecoderTest.java
+++ b/sax/src/test/java/feign/sax/SAXDecoderTest.java
@@ -136,11 +136,6 @@ public void characters(char ch[], int start, int length) {
     }
   }
 
-  @Test public void voidDecodesToNull() throws Exception {
-    Response response = Response.create(200, "OK", Collections.>emptyMap(), statusFailed);
-    assertEquals(decoder.decode(response, void.class), null);
-  }
-
   @Test public void nullBodyDecodesToNull() throws Exception {
     Response response = Response.create(204, "OK", Collections.>emptyMap(), null);
     assertEquals(decoder.decode(response, String.class), null);

From a73d69cfff52ea73e149866380e35e3d9f57c814 Mon Sep 17 00:00:00 2001
From: adriancole 
Date: Tue, 17 Sep 2013 16:22:46 -0700
Subject: [PATCH 113/179] removed dead constants

---
 core/src/main/java/feign/MethodHandler.java | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/core/src/main/java/feign/MethodHandler.java b/core/src/main/java/feign/MethodHandler.java
index b44a5fcff3..864759ef39 100644
--- a/core/src/main/java/feign/MethodHandler.java
+++ b/core/src/main/java/feign/MethodHandler.java
@@ -65,12 +65,6 @@ interface BuildTemplateFromArgs {
     public RequestTemplate apply(Object[] argv);
   }
 
-  /**
-   * same approach as retrofit: temporarily rename threads
-   */
-  static String THREAD_PREFIX = "Feign-";
-  static String IDLE_THREAD_NAME = THREAD_PREFIX + "Idle";
-
   static final class SynchronousMethodHandler implements MethodHandler {
 
     private final MethodMetadata metadata;

From d6e574dc814861d33d57e99a8f568a17ad3586e2 Mon Sep 17 00:00:00 2001
From: adriancole 
Date: Sat, 21 Sep 2013 08:38:14 -0700
Subject: [PATCH 114/179] address findbugs

---
 CHANGES.md                                    |  1 +
 core/src/main/java/feign/Client.java          |  2 +-
 core/src/main/java/feign/FeignException.java  |  4 ++--
 core/src/main/java/feign/Logger.java          |  5 ++---
 .../main/java/feign/RetryableException.java   |  8 +++----
 core/src/main/java/feign/Target.java          |  2 ++
 core/src/test/java/feign/FeignTest.java       | 22 +++++++++----------
 core/src/test/java/feign/LoggerTest.java      |  5 +++--
 core/src/test/java/feign/UtilTest.java        |  2 +-
 .../feign/ribbon/LoadBalancingTarget.java     |  2 ++
 .../feign/ribbon/LoadBalancingTargetTest.java |  5 +++--
 .../java/feign/ribbon/RibbonClientTest.java   |  5 +++--
 .../sax/examples/AWSSignatureVersion4.java    |  8 +++++--
 13 files changed, 41 insertions(+), 30 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 75cb7e0f28..c8ae4d2034 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,6 @@
 ### Version 5.0.1
 * `Decoder.decode()` is no longer called for `Response` or `void` types.
+* Miscellaneous findbugs fixes.
 
 ### Version 5.0
 * Remove support for Observable methods.
diff --git a/core/src/main/java/feign/Client.java b/core/src/main/java/feign/Client.java
index be6a0ba179..64e97682b8 100644
--- a/core/src/main/java/feign/Client.java
+++ b/core/src/main/java/feign/Client.java
@@ -144,7 +144,7 @@ Response convertResponse(HttpURLConnection connection) throws IOException {
       } else {
         stream = connection.getInputStream();
       }
-      Reader body = stream != null ? new InputStreamReader(stream) : null;
+      Reader body = stream != null ? new InputStreamReader(stream, UTF_8) : null;
       return Response.create(status, reason, headers, body, length);
     }
   }
diff --git a/core/src/main/java/feign/FeignException.java b/core/src/main/java/feign/FeignException.java
index 9d47141088..b014d71130 100644
--- a/core/src/main/java/feign/FeignException.java
+++ b/core/src/main/java/feign/FeignException.java
@@ -23,8 +23,8 @@
  * Origin exception type for all Http Apis.
  */
 public class FeignException extends RuntimeException {
-  static FeignException errorReading(Request request, Response response, IOException cause) {
-    return new FeignException(format("%s %s %s", cause.getMessage(), request.method(), request.url(), 0), cause);
+  static FeignException errorReading(Request request, Response ignored, IOException cause) {
+    return new FeignException(format("%s %s %s", cause.getMessage(), request.method(), request.url()), cause);
   }
 
   public static FeignException errorStatus(String methodKey, Response response) {
diff --git a/core/src/main/java/feign/Logger.java b/core/src/main/java/feign/Logger.java
index cd99901c86..b07e206453 100644
--- a/core/src/main/java/feign/Logger.java
+++ b/core/src/main/java/feign/Logger.java
@@ -176,10 +176,9 @@ Response logAndRebufferResponse(String configKey, Level logLevel, Response respo
           log(configKey, ""); // CRLF
         }
 
-        Reader body = response.body().asReader();
+        BufferedReader reader = new BufferedReader(response.body().asReader());
         try {
           StringBuilder buffered = new StringBuilder();
-          BufferedReader reader = new BufferedReader(body);
           String line;
           while ((line = reader.readLine()) != null) {
             buffered.append(line);
@@ -191,7 +190,7 @@ Response logAndRebufferResponse(String configKey, Level logLevel, Response respo
           log(configKey, "<--- END HTTP (%s-byte body)", bodyAsString.getBytes(UTF_8).length);
           return Response.create(response.status(), response.reason(), response.headers(), bodyAsString);
         } finally {
-          ensureClosed(body);
+          ensureClosed(reader);
         }
       }
     }
diff --git a/core/src/main/java/feign/RetryableException.java b/core/src/main/java/feign/RetryableException.java
index f5d6eabb9b..d812cbc1e3 100644
--- a/core/src/main/java/feign/RetryableException.java
+++ b/core/src/main/java/feign/RetryableException.java
@@ -26,7 +26,7 @@ public class RetryableException extends FeignException {
 
   private static final long serialVersionUID = 1L;
 
-  private final Date retryAfter;
+  private final Long retryAfter;
 
   /**
    * @param retryAfter usually corresponds to the {@link feign.Util#RETRY_AFTER}
@@ -34,7 +34,7 @@ public class RetryableException extends FeignException {
    */
   public RetryableException(String message, Throwable cause, Date retryAfter) {
     super(message, cause);
-    this.retryAfter = retryAfter;
+    this.retryAfter = retryAfter != null ? retryAfter.getTime() : null;
   }
 
   /**
@@ -43,7 +43,7 @@ public RetryableException(String message, Throwable cause, Date retryAfter) {
    */
   public RetryableException(String message, Date retryAfter) {
     super(message);
-    this.retryAfter = retryAfter;
+    this.retryAfter = retryAfter != null ? retryAfter.getTime() : null;
   }
 
   /**
@@ -52,6 +52,6 @@ public RetryableException(String message, Date retryAfter) {
    * application-specific response.  Null if unknown.
    */
   public Date retryAfter() {
-    return retryAfter;
+    return retryAfter != null ? new Date(retryAfter) : null;
   }
 }
diff --git a/core/src/main/java/feign/Target.java b/core/src/main/java/feign/Target.java
index ab3588cce4..894855d472 100644
--- a/core/src/main/java/feign/Target.java
+++ b/core/src/main/java/feign/Target.java
@@ -100,6 +100,8 @@ public HardCodedTarget(Class type, String name, String url) {
     }
 
     @Override public boolean equals(Object obj) {
+      if (obj == null)
+        return false;
       if (this == obj)
         return true;
       if (HardCodedTarget.class != obj.getClass())
diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java
index 495efc4b92..fa86f19da5 100644
--- a/core/src/test/java/feign/FeignTest.java
+++ b/core/src/test/java/feign/FeignTest.java
@@ -136,7 +136,7 @@ public void postTemplateParamsResolve() throws IOException, InterruptedException
       TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module());
 
       api.login("netflix", "denominator", "password");
-      assertEquals(new String(server.takeRequest().getBody()),
+      assertEquals(new String(server.takeRequest().getBody(), UTF_8),
           "{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}");
     } finally {
       server.shutdown();
@@ -171,7 +171,7 @@ public void postFormParams() throws IOException, InterruptedException {
       TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module());
 
       api.form("netflix", "denominator", "password");
-      assertEquals(new String(server.takeRequest().getBody()),
+      assertEquals(new String(server.takeRequest().getBody(), UTF_8),
           "customer_name=netflix,user_name=denominator,password=password");
     } finally {
       server.shutdown();
@@ -190,7 +190,7 @@ public void postBodyParam() throws IOException, InterruptedException {
       api.body(Arrays.asList("netflix", "denominator", "password"));
       RecordedRequest request = server.takeRequest();
       assertEquals(request.getHeader("Content-Length"), "32");
-      assertEquals(new String(request.getBody()), "[netflix, denominator, password]");
+      assertEquals(new String(request.getBody(), UTF_8), "[netflix, denominator, password]");
     } finally {
       server.shutdown();
     }
@@ -317,7 +317,7 @@ public void canOverrideErrorDecoder() throws IOException, InterruptedException {
   @Test public void retriesLostConnectionBeforeRead() throws IOException, InterruptedException {
     MockWebServer server = new MockWebServer();
     server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START));
-    server.enqueue(new MockResponse().setBody("success!".getBytes()));
+    server.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8)));
     server.play();
 
     try {
@@ -346,7 +346,7 @@ public Object decode(Response response, Type type) {
 
   public void overrideTypeSpecificDecoder() throws IOException, InterruptedException {
     MockWebServer server = new MockWebServer();
-    server.enqueue(new MockResponse().setBody("success!".getBytes()));
+    server.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8)));
     server.play();
 
     try {
@@ -380,8 +380,8 @@ public Object decode(Response response, Type type) throws IOException, FeignExce
    */
   public void retryableExceptionInDecoder() throws IOException, InterruptedException {
     MockWebServer server = new MockWebServer();
-    server.enqueue(new MockResponse().setBody("retry!".getBytes()));
-    server.enqueue(new MockResponse().setBody("success!".getBytes()));
+    server.enqueue(new MockResponse().setBody("retry!".getBytes(UTF_8)));
+    server.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8)));
     server.play();
 
     try {
@@ -410,7 +410,7 @@ public Object decode(Response response, Type type) throws IOException {
   @Test(expectedExceptions = FeignException.class, expectedExceptionsMessageRegExp = "error reading response POST http://.*")
   public void doesntRetryAfterResponseIsSent() throws IOException, InterruptedException {
     MockWebServer server = new MockWebServer();
-    server.enqueue(new MockResponse().setBody("success!".getBytes()));
+    server.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8)));
     server.play();
 
     try {
@@ -434,7 +434,7 @@ static class TrustSSLSockets {
   @Test public void canOverrideSSLSocketFactory() throws IOException, InterruptedException {
     MockWebServer server = new MockWebServer();
     server.useHttps(TrustingSSLSocketFactory.get("localhost"), false);
-    server.enqueue(new MockResponse().setBody("success!".getBytes()));
+    server.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8)));
     server.play();
 
     try {
@@ -456,7 +456,7 @@ static class DisableHostnameVerification {
   @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.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8)));
     server.play();
 
     try {
@@ -472,7 +472,7 @@ static class DisableHostnameVerification {
     MockWebServer server = new MockWebServer();
     server.useHttps(TrustingSSLSocketFactory.get("localhost"), false);
     server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
-    server.enqueue(new MockResponse().setBody("success!".getBytes()));
+    server.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8)));
     server.play();
 
     try {
diff --git a/core/src/test/java/feign/LoggerTest.java b/core/src/test/java/feign/LoggerTest.java
index 3a3fa41def..0d32a36d11 100644
--- a/core/src/test/java/feign/LoggerTest.java
+++ b/core/src/test/java/feign/LoggerTest.java
@@ -33,6 +33,7 @@
 import java.util.List;
 import java.util.regex.Pattern;
 
+import static feign.Util.UTF_8;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertTrue;
 import static org.testng.Assert.fail;
@@ -116,7 +117,7 @@ public void levelEmits(final Logger.Level logLevel, List expectedMessage
         assertTrue(messages.get(i).matches(expectedMessages.get(i)), messages.get(i));
       }
 
-      assertEquals(new String(server.takeRequest().getBody()),
+      assertEquals(new String(server.takeRequest().getBody(), UTF_8),
           "{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}");
     } finally {
       server.shutdown();
@@ -213,7 +214,7 @@ public void readTimeoutEmits(final Logger.Level logLevel, List expectedM
 
       assertMessagesMatch(expectedMessages);
 
-      assertEquals(new String(server.takeRequest().getBody()),
+      assertEquals(new String(server.takeRequest().getBody(), UTF_8),
           "{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}");
     } finally {
       server.shutdown();
diff --git a/core/src/test/java/feign/UtilTest.java b/core/src/test/java/feign/UtilTest.java
index 322bffe4cc..72fd77b201 100644
--- a/core/src/test/java/feign/UtilTest.java
+++ b/core/src/test/java/feign/UtilTest.java
@@ -42,7 +42,7 @@ interface ParameterizedDecoder> extends Decoder {
   interface Parameterized {
   }
 
-  class ParameterizedSubtype implements Parameterized {
+  static class ParameterizedSubtype implements Parameterized {
   }
 
   @Test public void resolveLastTypeParameterWhenNotSubtype() throws Exception {
diff --git a/ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java b/ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java
index c10aa5e6b3..0894ed4817 100644
--- a/ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java
+++ b/ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java
@@ -104,6 +104,8 @@ public AbstractLoadBalancer lb() {
   }
 
   @Override public boolean equals(Object obj) {
+    if (obj == null)
+      return false;
     if (this == obj)
       return true;
     if (LoadBalancingTarget.class != obj.getClass())
diff --git a/ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java b/ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java
index befef3c7a9..70c34bc8f8 100644
--- a/ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java
+++ b/ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java
@@ -27,6 +27,7 @@
 import feign.RequestLine;
 
 import static com.netflix.config.ConfigurationManager.getConfigInstance;
+import static feign.Util.UTF_8;
 import static org.testng.Assert.assertEquals;
 
 @Test
@@ -41,10 +42,10 @@ public void loadBalancingDefaultPolicyRoundRobin() throws IOException, Interrupt
     String serverListKey = name + ".ribbon.listOfServers";
 
     MockWebServer server1 = new MockWebServer();
-    server1.enqueue(new MockResponse().setResponseCode(200).setBody("success!".getBytes()));
+    server1.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8)));
     server1.play();
     MockWebServer server2 = new MockWebServer();
-    server2.enqueue(new MockResponse().setResponseCode(200).setBody("success!".getBytes()));
+    server2.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8)));
     server2.play();
 
     getConfigInstance().setProperty(serverListKey, hostAndPort(server1.getUrl("")) + "," + hostAndPort(server2.getUrl("")));
diff --git a/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java b/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java
index d16a738ce6..f5cc14c170 100644
--- a/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java
+++ b/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java
@@ -28,6 +28,7 @@
 import java.net.URL;
 
 import static com.netflix.config.ConfigurationManager.getConfigInstance;
+import static feign.Util.UTF_8;
 import static org.testng.Assert.assertEquals;
 
 @Test
@@ -53,10 +54,10 @@ public void loadBalancingDefaultPolicyRoundRobin() throws IOException, Interrupt
     String serverListKey = client + ".ribbon.listOfServers";
 
     MockWebServer server1 = new MockWebServer();
-    server1.enqueue(new MockResponse().setResponseCode(200).setBody("success!".getBytes()));
+    server1.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8)));
     server1.play();
     MockWebServer server2 = new MockWebServer();
-    server2.enqueue(new MockResponse().setResponseCode(200).setBody("success!".getBytes()));
+    server2.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8)));
     server2.play();
 
     getConfigInstance().setProperty(serverListKey, hostAndPort(server1.getUrl("")) + "," + hostAndPort(server2.getUrl("")));
diff --git a/sax/src/test/java/feign/sax/examples/AWSSignatureVersion4.java b/sax/src/test/java/feign/sax/examples/AWSSignatureVersion4.java
index d9751675ff..282be5f786 100644
--- a/sax/src/test/java/feign/sax/examples/AWSSignatureVersion4.java
+++ b/sax/src/test/java/feign/sax/examples/AWSSignatureVersion4.java
@@ -60,7 +60,11 @@ public AWSSignatureVersion4(String accessKey, String secretKey) {
           transform(input.headers().get(key), trimToLowercase));
     }
 
-    String timestamp = iso8601.format(new Date());
+    String timestamp;
+    synchronized (iso8601) {
+      timestamp = iso8601.format(new Date());
+    }
+
     String credentialScope = Joiner.on('/').join(timestamp.substring(0, 8), region, service, "aws4_request");
 
     input.query("X-Amz-Algorithm", "AWS4-HMAC-SHA256");
@@ -135,7 +139,7 @@ private String canonicalString(RequestTemplate input, Multimap s
 
   private static final Function trimToLowercase = new Function() {
     public String apply(String in) {
-      return in.toLowerCase().trim();
+      return in == null ? null : in.toLowerCase().trim();
     }
   };
 

From ef2a6b9e9c4afd8d73d15c120461b23dfd676633 Mon Sep 17 00:00:00 2001
From: adriancole 
Date: Sat, 21 Sep 2013 09:14:59 -0700
Subject: [PATCH 115/179] fixed CHANGES version

---
 CHANGES.md | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/CHANGES.md b/CHANGES.md
index c8ae4d2034..1325dd532f 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,6 +1,8 @@
+### Version 5.1.0
+* Miscellaneous findbugs fixes.
+
 ### Version 5.0.1
 * `Decoder.decode()` is no longer called for `Response` or `void` types.
-* Miscellaneous findbugs fixes.
 
 ### Version 5.0
 * Remove support for Observable methods.

From e865f94c95618391adcec250cdfbda8e931c1eb8 Mon Sep 17 00:00:00 2001
From: adriancole 
Date: Sat, 21 Sep 2013 09:16:43 -0700
Subject: [PATCH 116/179] Correctly handle IOExceptions wrapped by Ribbon.

---
 CHANGES.md                                    |  1 +
 .../main/java/feign/ribbon/RibbonModule.java  |  3 ++
 .../java/feign/ribbon/RibbonClientTest.java   | 30 ++++++++++++++++++-
 3 files changed, 33 insertions(+), 1 deletion(-)

diff --git a/CHANGES.md b/CHANGES.md
index 1325dd532f..cc2d66ea16 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,4 +1,5 @@
 ### Version 5.1.0
+* Correctly handle IOExceptions wrapped by Ribbon.
 * Miscellaneous findbugs fixes.
 
 ### Version 5.0.1
diff --git a/ribbon/src/main/java/feign/ribbon/RibbonModule.java b/ribbon/src/main/java/feign/ribbon/RibbonModule.java
index 9054d10139..5dc36aeb75 100644
--- a/ribbon/src/main/java/feign/ribbon/RibbonModule.java
+++ b/ribbon/src/main/java/feign/ribbon/RibbonModule.java
@@ -76,6 +76,9 @@ public RibbonClient(@Named("delegate") Client delegate) {
         LBClient.RibbonRequest ribbonRequest = new LBClient.RibbonRequest(request, uriWithoutSchemeAndPort);
         return lbClient(clientName).executeWithLoadBalancer(ribbonRequest).toResponse();
       } catch (ClientException e) {
+        if (e.getCause() instanceof IOException) {
+          throw IOException.class.cast(e.getCause());
+        }
         throw Throwables.propagate(e);
       }
     }
diff --git a/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java b/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java
index f5cc14c170..d691b94cc8 100644
--- a/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java
+++ b/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java
@@ -17,6 +17,7 @@
 
 import com.google.mockwebserver.MockResponse;
 import com.google.mockwebserver.MockWebServer;
+import com.google.mockwebserver.SocketPolicy;
 import dagger.Provides;
 import feign.Feign;
 import feign.RequestLine;
@@ -36,7 +37,7 @@ public class RibbonClientTest {
   interface TestInterface {
     @RequestLine("POST /") void post();
 
-    @dagger.Module(injects = Feign.class, addsTo = Feign.Defaults.class)
+    @dagger.Module(injects = Feign.class, overrides = true, addsTo = Feign.Defaults.class)
     static class Module {
       @Provides Decoder defaultDecoder() {
         return new Decoder.Default();
@@ -80,6 +81,33 @@ public void loadBalancingDefaultPolicyRoundRobin() throws IOException, Interrupt
     }
   }
 
+  @Test
+  public void ioExceptionRetry() throws IOException, InterruptedException {
+    String client = "RibbonClientTest-ioExceptionRetry";
+    String serverListKey = client + ".ribbon.listOfServers";
+
+    MockWebServer server = new MockWebServer();
+    server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START));
+    server.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8)));
+    server.play();
+
+    getConfigInstance().setProperty(serverListKey, hostAndPort(server.getUrl("")));
+
+    try {
+
+      TestInterface api = Feign.create(TestInterface.class, "http://" + client, new TestInterface.Module(), new RibbonModule());
+
+      api.post();
+
+      assertEquals(server.getRequestCount(), 2);
+      // TODO: verify ribbon stats match
+      // assertEquals(target.lb().getLoadBalancerStats().getSingleServerStat())
+    } finally {
+      server.shutdown();
+      getConfigInstance().clearProperty(serverListKey);
+    }
+  }
+
   static String hostAndPort(URL url) {
     // our build slaves have underscores in their hostnames which aren't permitted by ribbon
     return "localhost:" + url.getPort();

From eb03e2010e949fb30fed5c86395fe8b9c451fb2a Mon Sep 17 00:00:00 2001
From: adriancole 
Date: Sat, 21 Sep 2013 11:24:38 -0700
Subject: [PATCH 117/179] updated docs on decoder

---
 README.md                                   |  2 +-
 core/src/main/java/feign/codec/Decoder.java | 17 ++++++++++++++---
 2 files changed, 15 insertions(+), 4 deletions(-)

diff --git a/README.md b/README.md
index e0a8db5fb6..4afd717ea7 100644
--- a/README.md
+++ b/README.md
@@ -121,7 +121,7 @@ MyService api = Feign.create(MyService.class, "https://myAppProd", new RibbonMod
 ### Decoders
 The last argument to `Feign.create` allows you to specify additional configuration such as how to decode a responses, modeled in Dagger.
 
-If any methods in your interface return types besides `Response`, `void` or `String`, you'll need to configure a `Decoder`.
+If any methods in your interface return types besides `Response` or `void`, you'll need to configure a `Decoder`.
 
 The `GsonModule` in the `feign-gson` extension configures a `Decoder` which parses objects from JSON using reflection.
 
diff --git a/core/src/main/java/feign/codec/Decoder.java b/core/src/main/java/feign/codec/Decoder.java
index 0c20a51389..94396ff3d9 100644
--- a/core/src/main/java/feign/codec/Decoder.java
+++ b/core/src/main/java/feign/codec/Decoder.java
@@ -25,7 +25,7 @@
 import static java.lang.String.format;
 
 /**
- * Decodes an HTTP response into a single object of the given {@code Type}. Invoked when
+ * Decodes an HTTP response into a single object of the given {@code type}. Invoked when
  * {@link Response#status()} is in the 2xx range and the return type is neither {@code void} nor {@code Response}.
  * 

*

@@ -49,14 +49,25 @@ * } * } *

+ *
+ *

Implementation Note

+ * The {@code type} parameter will correspond to the + * {@link java.lang.reflect.Method#getGenericReturnType() generic return type} + * of an {@link feign.Target#type() interface} processed by + * {@link feign.Feign#newInstance(feign.Target)}. When writing your + * implementation of Decoder, ensure you also test parameterized types such as + * {@code List}. + * */ public interface Decoder { /** - * Decodes a response into a single object. + * Decodes an http response into an object corresponding to its + * {@link java.lang.reflect.Method#getGenericReturnType() generic return type}. * If you need to wrap exceptions, please do so via {@link DecodeException}. * * @param response the response to decode - * @param type Target object type. + * @param type {@link java.lang.reflect.Method#getGenericReturnType() generic return type} + * of the method corresponding to this {@code response}. * @return instance of {@code type} * @throws IOException will be propagated safely to the caller. * @throws DecodeException when decoding failed due to a checked exception besides IOException. From 7fb20174fbe14be81b06618b2667748b68d2df8b Mon Sep 17 00:00:00 2001 From: adriancole Date: Sun, 22 Sep 2013 15:24:47 -0700 Subject: [PATCH 118/179] support usage of gson without using dagger --- CHANGES.md | 3 ++ README.md | 51 ++++++++----------- gson/README.md | 12 ++++- gson/src/main/java/feign/gson/GsonCodec.java | 48 +++++++++++++++++ gson/src/main/java/feign/gson/GsonModule.java | 49 ++---------------- .../test/java/feign/gson/GsonModuleTest.java | 4 +- .../feign/gson/examples/GitHubExample.java | 4 +- 7 files changed, 90 insertions(+), 81 deletions(-) create mode 100644 gson/src/main/java/feign/gson/GsonCodec.java diff --git a/CHANGES.md b/CHANGES.md index cc2d66ea16..2e1f435691 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,6 @@ +### Version 5.2.0 +* Support usage of `GsonCodec` via `Feign.Builder` + ### Version 5.1.0 * Correctly handle IOExceptions wrapped by Ribbon. * Miscellaneous findbugs fixes. diff --git a/README.md b/README.md index 4afd717ea7..229652f1be 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,9 @@ static class Contributor { } public static void main(String... args) { - GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule()); + GitHub github = Feign.builder() + .decoder(new GsonCodec()) + .target(GitHub.class, "https://api.github.com"); // Fetch and print a list of the contributors to this library. List contributors = github.contributors("netflix", "feign"); @@ -35,8 +37,6 @@ 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. - ### Customization Feign has several aspects that can be customized. For simple cases, you can use `Feign.builder()` to construct an API interface with your custom components. For example: @@ -83,9 +83,14 @@ Feign intends to work well within Netflix and other Open Source communities. Mo ### Gson [GsonModule](https://github.com/Netflix/feign/tree/master/gson) adds default encoders and decoders so you get get started with a JSON api. -Integration requires you pass `new GsonModule()` to `Feign.create()`, or add it to your graph with Dagger: +Add `GsonCodec` to your `Feign.Builder` like so: + ```java -GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule()); +GsonCodec codec = new GsonCodec(); +GitHub github = Feign.builder() + .encoder(codec) + .decoder(codec) + .target(GitHub.class, "https://api.github.com"); ``` ### Sax @@ -119,26 +124,16 @@ MyService api = Feign.create(MyService.class, "https://myAppProd", new RibbonMod ``` ### Decoders -The last argument to `Feign.create` allows you to specify additional configuration such as how to decode a responses, modeled in Dagger. +`Feign.builder()` allows you to specify additional configuration such as how to decode a response. If any methods in your interface return types besides `Response` or `void`, you'll need to configure a `Decoder`. -The `GsonModule` in the `feign-gson` extension configures a `Decoder` which parses objects from JSON using reflection. +Here's how to configure json decoding (using the `feign-gson` extension): -Here's how you could write this yourself, using whatever library you prefer: ```java -@Module(library = true) -static class JsonModule { - @Provides Decoder decoder(final JsonParser parser) { - return new Decoder() { - - @Override public Object decode(Response response, Type type) throws IOException { - return parser.readJson(response.body().asReader(), type); - } - - }; - } -} +GitHub github = Feign.builder() + .decoder(new GsonCodec()) + .target(GitHub.class, "https://api.github.com"); ``` ### Advanced usage and Dagger @@ -166,15 +161,9 @@ Where possible, Feign configuration uses normal Dagger conventions. For example #### Logging You can log the http messages going to and from the target by setting up a `Logger`. Here's the easiest way to do that: ```java -@Module(overrides = true) -class Overrides { - @Provides @Singleton Logger.Level provideLoggerLevel() { - return Logger.Level.FULL; - } - - @Provides @Singleton Logger provideLogger() { - return new Logger.JavaLogger().appendToFile("logs/http.log"); - } -} -GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonGitHubModule(), new Overrides()); +GitHub github = Feign.builder() + .decoder(new GsonCodec()) + .logger(new Logger.JavaLogger().appendToFile("logs/http.log")) + .logLevel(Logger.Level.FULL) + .target(GitHub.class, "https://api.github.com"); ``` diff --git a/gson/README.md b/gson/README.md index 206990e74b..09b3464048 100644 --- a/gson/README.md +++ b/gson/README.md @@ -3,7 +3,17 @@ Gson Codec This module adds support for encoding and decoding json via the Gson library. -Add this to your object graph like so: +Add `GsonCodec` to your `Feign.Builder` like so: + +```java +GsonCodec codec = new GsonCodec(); +GitHub github = Feign.builder() + .encoder(codec) + .decoder(codec) + .target(GitHub.class, "https://api.github.com"); +``` + +Or.. to your object graph like so: ```java GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule()); diff --git a/gson/src/main/java/feign/gson/GsonCodec.java b/gson/src/main/java/feign/gson/GsonCodec.java new file mode 100644 index 0000000000..649d7e00f9 --- /dev/null +++ b/gson/src/main/java/feign/gson/GsonCodec.java @@ -0,0 +1,48 @@ +package feign.gson; + +import com.google.gson.Gson; +import com.google.gson.JsonIOException; +import feign.RequestTemplate; +import feign.Response; +import feign.codec.Decoder; +import feign.codec.Encoder; + +import javax.inject.Inject; +import java.io.IOException; +import java.io.Reader; +import java.lang.reflect.Type; + +import static feign.Util.ensureClosed; + +public class GsonCodec implements Encoder, Decoder { + private final Gson gson; + + public GsonCodec() { + this(new Gson()); + } + + @Inject public GsonCodec(Gson gson) { + this.gson = gson; + } + + @Override public void encode(Object object, RequestTemplate template) { + template.body(gson.toJson(object)); + } + + @Override public Object decode(Response response, Type type) throws IOException { + if (response.body() == null) { + return null; + } + Reader reader = response.body().asReader(); + try { + return gson.fromJson(reader, type); + } catch (JsonIOException e) { + if (e.getCause() != null && e.getCause() instanceof IOException) { + throw IOException.class.cast(e.getCause()); + } + throw e; + } finally { + ensureClosed(reader); + } + } +} diff --git a/gson/src/main/java/feign/gson/GsonModule.java b/gson/src/main/java/feign/gson/GsonModule.java index cd3a031030..c4d7d6c4a4 100644 --- a/gson/src/main/java/feign/gson/GsonModule.java +++ b/gson/src/main/java/feign/gson/GsonModule.java @@ -18,7 +18,6 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.InstanceCreator; -import com.google.gson.JsonIOException; import com.google.gson.TypeAdapter; import com.google.gson.internal.ConstructorConstructor; import com.google.gson.internal.bind.MapTypeAdapterFactory; @@ -27,21 +26,16 @@ import com.google.gson.stream.JsonWriter; import dagger.Provides; import feign.Feign; -import feign.RequestTemplate; -import feign.Response; import feign.codec.Decoder; import feign.codec.Encoder; -import javax.inject.Inject; import javax.inject.Singleton; import java.io.IOException; -import java.io.Reader; import java.lang.reflect.Type; import java.util.Collections; import java.util.Map; import java.util.Set; -import static feign.Util.ensureClosed; import static feign.Util.resolveLastTypeParameter; /** @@ -52,20 +46,20 @@ * to read numbers in a {@code Map} as Integers. You can * customize further by adding additional set bindings to the raw type * {@code TypeAdapter}. - * + *

*
* Here's an example of adding a custom json type adapter. - * + *

*

  * @Provides(type = Provides.Type.SET)
  * TypeAdapter upperZone() {
  *     return new TypeAdapter<Zone>() {
- * 
+ *
  *         @Override
  *         public void write(JsonWriter out, Zone value) throws IOException {
  *             throw new IllegalArgumentException();
  *         }
- * 
+ *
  *         @Override
  *         public Zone read(JsonReader in) throws IOException {
  *             in.beginObject();
@@ -91,41 +85,6 @@ public final class GsonModule {
     return codec;
   }
 
-  static class GsonCodec implements Encoder, Decoder {
-    private final Gson gson;
-
-    @Inject GsonCodec(Gson gson) {
-      this.gson = gson;
-    }
-
-    @Override public void encode(Object object, RequestTemplate template) {
-      template.body(gson.toJson(object));
-    }
-
-    @Override public Object decode(Response response, Type type) throws IOException {
-      if (response.body() == null) {
-        return null;
-      }
-      Reader reader = response.body().asReader();
-      try {
-        return fromJson(new JsonReader(reader), type);
-      } finally {
-        ensureClosed(reader);
-      }
-    }
-
-    private Object fromJson(JsonReader jsonReader, Type type) throws IOException {
-      try {
-        return gson.fromJson(jsonReader, type);
-      } catch (JsonIOException e) {
-        if (e.getCause() != null && e.getCause() instanceof IOException) {
-          throw IOException.class.cast(e.getCause());
-        }
-        throw e;
-      }
-    }
-  }
-
   @Provides @Singleton Gson gson(Set adapters) {
     GsonBuilder builder = new GsonBuilder().setPrettyPrinting();
     for (TypeAdapter adapter : adapters) {
diff --git a/gson/src/test/java/feign/gson/GsonModuleTest.java b/gson/src/test/java/feign/gson/GsonModuleTest.java
index e8a23d76fd..75c189ffde 100644
--- a/gson/src/test/java/feign/gson/GsonModuleTest.java
+++ b/gson/src/test/java/feign/gson/GsonModuleTest.java
@@ -52,8 +52,8 @@ static class EncoderAndDecoderBindings {
     EncoderAndDecoderBindings bindings = new EncoderAndDecoderBindings();
     ObjectGraph.create(bindings).inject(bindings);
 
-    assertEquals(bindings.encoder.getClass(), GsonModule.GsonCodec.class);
-    assertEquals(bindings.decoder.getClass(), GsonModule.GsonCodec.class);
+    assertEquals(bindings.encoder.getClass(), GsonCodec.class);
+    assertEquals(bindings.decoder.getClass(), GsonCodec.class);
   }
 
   @Module(includes = GsonModule.class, injects = EncoderBindings.class)
diff --git a/gson/src/test/java/feign/gson/examples/GitHubExample.java b/gson/src/test/java/feign/gson/examples/GitHubExample.java
index 66fa719496..ea12f85d0a 100644
--- a/gson/src/test/java/feign/gson/examples/GitHubExample.java
+++ b/gson/src/test/java/feign/gson/examples/GitHubExample.java
@@ -17,7 +17,7 @@
 
 import feign.Feign;
 import feign.RequestLine;
-import feign.gson.GsonModule;
+import feign.gson.GsonCodec;
 
 import javax.inject.Named;
 import java.util.List;
@@ -38,7 +38,7 @@ static class Contributor {
   }
 
   public static void main(String... args) throws InterruptedException {
-    GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule());
+    GitHub github = Feign.builder().decoder(new GsonCodec()).target(GitHub.class, "https://api.github.com");
 
     System.out.println("Let's fetch and print a list of the contributors to this library.");
     List contributors = github.contributors("netflix", "feign");

From a4583722baeedeac35cd472a36c43a62bd8b8234 Mon Sep 17 00:00:00 2001
From: "David M. Carr" 
Date: Sun, 22 Sep 2013 19:02:26 -0400
Subject: [PATCH 119/179] Simplify Decoder.Default by extending StringDecoder

Previously, the default decoder had logic relating to responses that made it distinct from StringDecoder.
Now that that's handled elsewhere, the body of the decode methods had less meaningful differences.
I opted to maintain Decoder.Default for consistency with other default implementations.  StringDecoder
is maintained as a separate class for backwards compatibility, and because it may be useful in the
future for clients to use a plain String decoder even if the default decoder starts having additional
capabilities.
---
 core/src/main/java/feign/Util.java               |  3 +++
 core/src/main/java/feign/codec/Decoder.java      | 16 ++--------------
 .../src/main/java/feign/codec/StringDecoder.java | 10 ++++++----
 .../java/feign/codec/DefaultDecoderTest.java     |  1 -
 4 files changed, 11 insertions(+), 19 deletions(-)

diff --git a/core/src/main/java/feign/Util.java b/core/src/main/java/feign/Util.java
index 412b10f66c..f3b7b0ac6f 100644
--- a/core/src/main/java/feign/Util.java
+++ b/core/src/main/java/feign/Util.java
@@ -168,6 +168,9 @@ public static Type resolveLastTypeParameter(Type genericContext, Class supert
 
   private static final int BUF_SIZE = 0x800; // 2K chars (4K bytes)
 
+  /**
+   * Adapted from {@code com.google.common.io.CharStreams.toString()}.
+   */
   public static String toString(Reader reader) throws IOException {
     if (reader == null) {
       return null;
diff --git a/core/src/main/java/feign/codec/Decoder.java b/core/src/main/java/feign/codec/Decoder.java
index 94396ff3d9..8167854c26 100644
--- a/core/src/main/java/feign/codec/Decoder.java
+++ b/core/src/main/java/feign/codec/Decoder.java
@@ -17,13 +17,10 @@
 
 import feign.FeignException;
 import feign.Response;
-import feign.Util;
 
 import java.io.IOException;
 import java.lang.reflect.Type;
 
-import static java.lang.String.format;
-
 /**
  * Decodes an HTTP response into a single object of the given {@code type}. Invoked when
  * {@link Response#status()} is in the 2xx range and the return type is neither {@code void} nor {@code Response}.
@@ -76,17 +73,8 @@ public interface Decoder {
   Object decode(Response response, Type type) throws IOException, DecodeException, FeignException;
 
   /**
-   * Default implementation of {@code Decoder} that supports {@code String} signatures.
+   * Default implementation of {@code Decoder}.
    */
-  public class Default implements Decoder {
-    @Override
-    public Object decode(Response response, Type type) throws IOException {
-      if (response.body() == null) {
-        return null;
-      } else if (String.class.equals(type)) {
-        return Util.toString(response.body().asReader());
-      }
-      throw new DecodeException(format("%s is not a type supported by this decoder.", type));
-    }
+  public class Default extends StringDecoder {
   }
 }
diff --git a/core/src/main/java/feign/codec/StringDecoder.java b/core/src/main/java/feign/codec/StringDecoder.java
index 03c1b1d3a6..ae35eca978 100644
--- a/core/src/main/java/feign/codec/StringDecoder.java
+++ b/core/src/main/java/feign/codec/StringDecoder.java
@@ -21,9 +21,8 @@
 import java.io.IOException;
 import java.lang.reflect.Type;
 
-/**
- * Adapted from {@code com.google.common.io.CharStreams.toString()}.
- */
+import static java.lang.String.format;
+
 public class StringDecoder implements Decoder {
   @Override
   public Object decode(Response response, Type type) throws IOException {
@@ -31,6 +30,9 @@ public Object decode(Response response, Type type) throws IOException {
     if (body == null) {
       return null;
     }
-    return Util.toString(body.asReader());
+    if (String.class.equals(type)) {
+      return Util.toString(body.asReader());
+    }
+    throw new DecodeException(format("%s is not a type supported by this decoder.", type));
   }
 }
diff --git a/core/src/test/java/feign/codec/DefaultDecoderTest.java b/core/src/test/java/feign/codec/DefaultDecoderTest.java
index 08da68bb4b..c5b58f8688 100644
--- a/core/src/test/java/feign/codec/DefaultDecoderTest.java
+++ b/core/src/test/java/feign/codec/DefaultDecoderTest.java
@@ -16,7 +16,6 @@
 package feign.codec;
 
 import feign.Response;
-import feign.Util;
 import org.testng.annotations.Test;
 import org.w3c.dom.Document;
 

From fe5e8d150b64874c09f2cb273da97bb9c57aec7c Mon Sep 17 00:00:00 2001
From: "David M. Carr" 
Date: Sun, 22 Sep 2013 19:43:21 -0400
Subject: [PATCH 120/179] Simplify usage of Gson from Feign.Builder

The logic in GsonCodec was split into GsonEncoder and GsonDecoder, each of which can
now be used separately.  GsonCodec was deprecated, and can be removed in the next major
version.  To facilitate use outside of Dagger, the double-to-int map type adapter was broken into
its own class, and is included by default when using the default constructors of either the
encoder or decoder.  The examples have been updated to use the new encoder/decoder instead
of the codec.
---
 CHANGES.md                                    |  4 ++
 README.md                                     | 14 ++---
 gson/README.md                                | 11 ++--
 .../feign/gson/DoubleToIntMapTypeAdapter.java | 54 ++++++++++++++++++
 gson/src/main/java/feign/gson/GsonCodec.java  | 31 ++++------
 .../src/main/java/feign/gson/GsonDecoder.java | 56 +++++++++++++++++++
 .../src/main/java/feign/gson/GsonEncoder.java | 36 ++++++++++++
 gson/src/main/java/feign/gson/GsonModule.java | 43 ++------------
 .../test/java/feign/gson/GsonModuleTest.java  |  4 +-
 .../feign/gson/examples/GitHubExample.java    |  4 +-
 10 files changed, 182 insertions(+), 75 deletions(-)
 create mode 100644 gson/src/main/java/feign/gson/DoubleToIntMapTypeAdapter.java
 create mode 100644 gson/src/main/java/feign/gson/GsonDecoder.java
 create mode 100644 gson/src/main/java/feign/gson/GsonEncoder.java

diff --git a/CHANGES.md b/CHANGES.md
index 2e1f435691..07a2d77770 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,7 @@
+### Version 5.3.0
+* Split `GsonCodec` into `GsonEncoder` and `GsonDecoder`, which are easy to use with `Feign.Builder`
+* Deprecate `GsonCodec`
+
 ### Version 5.2.0
 * Support usage of `GsonCodec` via `Feign.Builder`
 
diff --git a/README.md b/README.md
index 229652f1be..622d458153 100644
--- a/README.md
+++ b/README.md
@@ -26,7 +26,7 @@ static class Contributor {
 
 public static void main(String... args) {
   GitHub github = Feign.builder()
-                       .decoder(new GsonCodec())
+                       .decoder(new GsonDecoder())
                        .target(GitHub.class, "https://api.github.com");
 
   // Fetch and print a list of the contributors to this library.
@@ -83,13 +83,13 @@ Feign intends to work well within Netflix and other Open Source communities.  Mo
 ### Gson
 [GsonModule](https://github.com/Netflix/feign/tree/master/gson) adds default encoders and decoders so you get get started with a JSON api.
 
-Add `GsonCodec` to your `Feign.Builder` like so:
+Add `GsonEncoder` and/or `GsonDecoder` to your `Feign.Builder` like so:
 
 ```java
 GsonCodec codec = new GsonCodec();
 GitHub github = Feign.builder()
-                     .encoder(codec)
-                     .decoder(codec)
+                     .encoder(new GsonEncoder())
+                     .decoder(new GsonDecoder())
                      .target(GitHub.class, "https://api.github.com");
 ```
 
@@ -126,13 +126,13 @@ MyService api = Feign.create(MyService.class, "https://myAppProd", new RibbonMod
 ### Decoders
 `Feign.builder()` allows you to specify additional configuration such as how to decode a response.
 
-If any methods in your interface return types besides `Response` or `void`, you'll need to configure a `Decoder`.
+If any methods in your interface return types besides `Response`, `String` or `void`, you'll need to configure a `Decoder`.
 
 Here's how to configure json decoding (using the `feign-gson` extension):
 
 ```java
 GitHub github = Feign.builder()
-                     .decoder(new GsonCodec())
+                     .decoder(new GsonDecoder())
                      .target(GitHub.class, "https://api.github.com");
 ```
 
@@ -162,7 +162,7 @@ Where possible, Feign configuration uses normal Dagger conventions.  For example
 You can log the http messages going to and from the target by setting up a `Logger`.  Here's the easiest way to do that:
 ```java
 GitHub github = Feign.builder()
-                     .decoder(new GsonCodec())
+                     .decoder(new GsonDecoder())
                      .logger(new Logger.JavaLogger().appendToFile("logs/http.log"))
                      .logLevel(Logger.Level.FULL)
                      .target(GitHub.class, "https://api.github.com");
diff --git a/gson/README.md b/gson/README.md
index 09b3464048..bc6a476887 100644
--- a/gson/README.md
+++ b/gson/README.md
@@ -1,19 +1,18 @@
 Gson Codec
 ===================
 
-This module adds support for encoding and decoding json via the Gson library.
+This module adds support for encoding and decoding JSON via the Gson library.
 
-Add `GsonCodec` to your `Feign.Builder` like so:
+Add `GsonEncoder` and/or `GsonDecoder` to your `Feign.Builder` like so:
 
 ```java
-GsonCodec codec = new GsonCodec();
 GitHub github = Feign.builder()
-                     .encoder(codec)
-                     .decoder(codec)
+                     .encoder(new GsonEncoder())
+                     .decoder(new GsonDecoder())
                      .target(GitHub.class, "https://api.github.com");
 ```
 
-Or.. to your object graph like so:
+Or add them to your Dagger object graph like so:
 
 ```java
 GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule());
diff --git a/gson/src/main/java/feign/gson/DoubleToIntMapTypeAdapter.java b/gson/src/main/java/feign/gson/DoubleToIntMapTypeAdapter.java
new file mode 100644
index 0000000000..3a92f4f8a5
--- /dev/null
+++ b/gson/src/main/java/feign/gson/DoubleToIntMapTypeAdapter.java
@@ -0,0 +1,54 @@
+/*
+ * 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.gson;
+
+import com.google.gson.Gson;
+import com.google.gson.InstanceCreator;
+import com.google.gson.TypeAdapter;
+import com.google.gson.internal.ConstructorConstructor;
+import com.google.gson.internal.bind.MapTypeAdapterFactory;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Deals with scenario where Gson Object type treats all numbers as doubles.
+ */
+public class DoubleToIntMapTypeAdapter extends TypeAdapter> {
+  final static TypeToken> token = new TypeToken>() {};
+
+  private final TypeAdapter> delegate = new MapTypeAdapterFactory(new ConstructorConstructor(
+      Collections.>emptyMap()), false).create(new Gson(), token);
+
+  @Override public void write(JsonWriter out, Map value) throws IOException {
+    delegate.write(out, value);
+  }
+
+  @Override public Map read(JsonReader in) throws IOException {
+    Map map = delegate.read(in);
+    for (Map.Entry entry : map.entrySet()) {
+      if (entry.getValue() instanceof Double) {
+        entry.setValue(Double.class.cast(entry.getValue()).intValue());
+      }
+    }
+    return map;
+  }
+}
diff --git a/gson/src/main/java/feign/gson/GsonCodec.java b/gson/src/main/java/feign/gson/GsonCodec.java
index 649d7e00f9..b6ef12be1e 100644
--- a/gson/src/main/java/feign/gson/GsonCodec.java
+++ b/gson/src/main/java/feign/gson/GsonCodec.java
@@ -1,7 +1,6 @@
 package feign.gson;
 
 import com.google.gson.Gson;
-import com.google.gson.JsonIOException;
 import feign.RequestTemplate;
 import feign.Response;
 import feign.codec.Decoder;
@@ -9,40 +8,30 @@
 
 import javax.inject.Inject;
 import java.io.IOException;
-import java.io.Reader;
 import java.lang.reflect.Type;
 
-import static feign.Util.ensureClosed;
-
+/**
+ * @deprecated use {@link GsonEncoder} and {@link GsonDecoder} instead
+ */
+@Deprecated
 public class GsonCodec implements Encoder, Decoder {
-  private final Gson gson;
+  private final GsonEncoder encoder;
+  private final GsonDecoder decoder;
 
   public GsonCodec() {
     this(new Gson());
   }
 
   @Inject public GsonCodec(Gson gson) {
-    this.gson = gson;
+    this.encoder = new GsonEncoder(gson);
+    this.decoder = new GsonDecoder(gson);
   }
 
   @Override public void encode(Object object, RequestTemplate template) {
-    template.body(gson.toJson(object));
+    encoder.encode(object, template);
   }
 
   @Override public Object decode(Response response, Type type) throws IOException {
-    if (response.body() == null) {
-      return null;
-    }
-    Reader reader = response.body().asReader();
-    try {
-      return gson.fromJson(reader, type);
-    } catch (JsonIOException e) {
-      if (e.getCause() != null && e.getCause() instanceof IOException) {
-        throw IOException.class.cast(e.getCause());
-      }
-      throw e;
-    } finally {
-      ensureClosed(reader);
-    }
+    return decoder.decode(response, type);
   }
 }
diff --git a/gson/src/main/java/feign/gson/GsonDecoder.java b/gson/src/main/java/feign/gson/GsonDecoder.java
new file mode 100644
index 0000000000..66df54ea85
--- /dev/null
+++ b/gson/src/main/java/feign/gson/GsonDecoder.java
@@ -0,0 +1,56 @@
+/*
+ * 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.gson;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonIOException;
+import feign.Response;
+import feign.codec.Decoder;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.lang.reflect.Type;
+
+import static feign.Util.ensureClosed;
+
+public class GsonDecoder implements Decoder {
+  private final Gson gson;
+
+  public GsonDecoder() {
+    this(new Gson());
+  }
+
+  public GsonDecoder(Gson gson) {
+    this.gson = gson;
+  }
+
+  @Override public Object decode(Response response, Type type) throws IOException {
+    if (response.body() == null) {
+      return null;
+    }
+    Reader reader = response.body().asReader();
+    try {
+      return gson.fromJson(reader, type);
+    } catch (JsonIOException e) {
+      if (e.getCause() != null && e.getCause() instanceof IOException) {
+        throw IOException.class.cast(e.getCause());
+      }
+      throw e;
+    } finally {
+      ensureClosed(reader);
+    }
+  }
+}
diff --git a/gson/src/main/java/feign/gson/GsonEncoder.java b/gson/src/main/java/feign/gson/GsonEncoder.java
new file mode 100644
index 0000000000..4bee8df58b
--- /dev/null
+++ b/gson/src/main/java/feign/gson/GsonEncoder.java
@@ -0,0 +1,36 @@
+/*
+ * 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.gson;
+
+import com.google.gson.Gson;
+import feign.RequestTemplate;
+import feign.codec.Encoder;
+
+public class GsonEncoder implements Encoder {
+  private final Gson gson;
+
+  public GsonEncoder() {
+    this(new Gson());
+  }
+
+  public GsonEncoder(Gson gson) {
+    this.gson = gson;
+  }
+
+  @Override public void encode(Object object, RequestTemplate template) {
+    template.body(gson.toJson(object));
+  }
+}
diff --git a/gson/src/main/java/feign/gson/GsonModule.java b/gson/src/main/java/feign/gson/GsonModule.java
index c4d7d6c4a4..79093101f7 100644
--- a/gson/src/main/java/feign/gson/GsonModule.java
+++ b/gson/src/main/java/feign/gson/GsonModule.java
@@ -17,23 +17,15 @@
 
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
-import com.google.gson.InstanceCreator;
 import com.google.gson.TypeAdapter;
-import com.google.gson.internal.ConstructorConstructor;
-import com.google.gson.internal.bind.MapTypeAdapterFactory;
-import com.google.gson.reflect.TypeToken;
-import com.google.gson.stream.JsonReader;
-import com.google.gson.stream.JsonWriter;
 import dagger.Provides;
 import feign.Feign;
 import feign.codec.Decoder;
 import feign.codec.Encoder;
 
 import javax.inject.Singleton;
-import java.io.IOException;
 import java.lang.reflect.Type;
 import java.util.Collections;
-import java.util.Map;
 import java.util.Set;
 
 import static feign.Util.resolveLastTypeParameter;
@@ -77,12 +69,12 @@
 @dagger.Module(injects = Feign.class, addsTo = Feign.Defaults.class)
 public final class GsonModule {
 
-  @Provides Encoder encoder(GsonCodec codec) {
-    return codec;
+  @Provides Encoder encoder(Gson gson) {
+    return new GsonEncoder(gson);
   }
 
-  @Provides Decoder decoder(GsonCodec codec) {
-    return codec;
+  @Provides Decoder decoder(Gson gson) {
+    return new GsonDecoder(gson);
   }
 
   @Provides @Singleton Gson gson(Set adapters) {
@@ -94,30 +86,7 @@ public final class GsonModule {
     return builder.create();
   }
 
-  // deals with scenario where gson Object type treats all numbers as doubles.
-  @Provides(type = Provides.Type.SET) TypeAdapter doubleToInt() {
-    return new TypeAdapter>() {
-      TypeAdapter> delegate = new MapTypeAdapterFactory(new ConstructorConstructor(
-          Collections.>emptyMap()), false).create(new Gson(), token);
-
-      @Override
-      public void write(JsonWriter out, Map value) throws IOException {
-        delegate.write(out, value);
-      }
-
-      @Override
-      public Map read(JsonReader in) throws IOException {
-        Map map = delegate.read(in);
-        for (Map.Entry entry : map.entrySet()) {
-          if (entry.getValue() instanceof Double) {
-            entry.setValue(Double.class.cast(entry.getValue()).intValue());
-          }
-        }
-        return map;
-      }
-    }.nullSafe();
+  @Provides(type = Provides.Type.SET_VALUES) Set noDefaultTypeAdapters() {
+    return Collections.emptySet();
   }
-
-  private final static TypeToken> token = new TypeToken>() {
-  };
 }
diff --git a/gson/src/test/java/feign/gson/GsonModuleTest.java b/gson/src/test/java/feign/gson/GsonModuleTest.java
index 75c189ffde..2c94476105 100644
--- a/gson/src/test/java/feign/gson/GsonModuleTest.java
+++ b/gson/src/test/java/feign/gson/GsonModuleTest.java
@@ -52,8 +52,8 @@ static class EncoderAndDecoderBindings {
     EncoderAndDecoderBindings bindings = new EncoderAndDecoderBindings();
     ObjectGraph.create(bindings).inject(bindings);
 
-    assertEquals(bindings.encoder.getClass(), GsonCodec.class);
-    assertEquals(bindings.decoder.getClass(), GsonCodec.class);
+    assertEquals(bindings.encoder.getClass(), GsonEncoder.class);
+    assertEquals(bindings.decoder.getClass(), GsonDecoder.class);
   }
 
   @Module(includes = GsonModule.class, injects = EncoderBindings.class)
diff --git a/gson/src/test/java/feign/gson/examples/GitHubExample.java b/gson/src/test/java/feign/gson/examples/GitHubExample.java
index ea12f85d0a..6053ce51a5 100644
--- a/gson/src/test/java/feign/gson/examples/GitHubExample.java
+++ b/gson/src/test/java/feign/gson/examples/GitHubExample.java
@@ -17,7 +17,7 @@
 
 import feign.Feign;
 import feign.RequestLine;
-import feign.gson.GsonCodec;
+import feign.gson.GsonDecoder;
 
 import javax.inject.Named;
 import java.util.List;
@@ -38,7 +38,7 @@ static class Contributor {
   }
 
   public static void main(String... args) throws InterruptedException {
-    GitHub github = Feign.builder().decoder(new GsonCodec()).target(GitHub.class, "https://api.github.com");
+    GitHub github = Feign.builder().decoder(new GsonDecoder()).target(GitHub.class, "https://api.github.com");
 
     System.out.println("Let's fetch and print a list of the contributors to this library.");
     List contributors = github.contributors("netflix", "feign");

From 5452ccd7116288f3e84f29fd30b2c9ea518c4f51 Mon Sep 17 00:00:00 2001
From: adriancole 
Date: Mon, 23 Sep 2013 16:44:04 -0700
Subject: [PATCH 121/179] ribbon 0.2.3

---
 CHANGES.md   | 1 +
 build.gradle | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/CHANGES.md b/CHANGES.md
index 07a2d77770..e3e5806f0d 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,6 +1,7 @@
 ### Version 5.3.0
 * Split `GsonCodec` into `GsonEncoder` and `GsonDecoder`, which are easy to use with `Feign.Builder`
 * Deprecate `GsonCodec`
+* Update to Ribbon 0.2.3
 
 ### Version 5.2.0
 * Support usage of `GsonCodec` via `Feign.Builder`
diff --git a/build.gradle b/build.gradle
index 8a30186f8a..022cce7e8c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -98,7 +98,7 @@ project(':feign-ribbon') {
 
     dependencies {
         compile     project(':feign-core')
-        compile     'com.netflix.ribbon:ribbon-core:0.2.0'
+        compile     'com.netflix.ribbon:ribbon-core:0.2.3'
         testCompile 'org.testng:testng:6.8.5'
         testCompile 'com.google.mockwebserver:mockwebserver:20130706'
     }

From 28fabc89d2618fb154a5864365cfc4bced85f32a Mon Sep 17 00:00:00 2001
From: "David M. Carr" 
Date: Tue, 24 Sep 2013 19:34:47 -0400
Subject: [PATCH 122/179] add support for HTTP basic authentication (#79)

This changeset adds a simple request interceptor that performs HTTP basic authentication.

The HTTP spec isn't very clear on the use of character encodings within this header.
The most common interpretation in servers appears to be to expect ISO-8859-1, so I've
used that as a default, as well as allowing the encoding to be specified.

At @adriancole's suggestion, sun.misc.BASE64Encoder is used for the base64 encoding
rather than pulling an implementation into the Util class.  If we ever run into a JRE that
doesn't provide compatibility with that class, we can eliminate that dependency.
---
 CHANGES.md                                    |  3 +
 README.md                                     |  8 ++-
 core/src/main/java/feign/Util.java            |  4 ++
 .../auth/BasicAuthRequestInterceptor.java     | 69 +++++++++++++++++++
 .../auth/BasicAuthRequestInterceptorTest.java | 41 +++++++++++
 5 files changed, 124 insertions(+), 1 deletion(-)
 create mode 100644 core/src/main/java/feign/auth/BasicAuthRequestInterceptor.java
 create mode 100644 core/src/test/java/feign/auth/BasicAuthRequestInterceptorTest.java

diff --git a/CHANGES.md b/CHANGES.md
index e3e5806f0d..833a455239 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,6 @@
+### Version 5.4.0
+* Add `BasicAuthRequestInterceptor`
+
 ### Version 5.3.0
 * Split `GsonCodec` into `GsonEncoder` and `GsonDecoder`, which are easy to use with `Feign.Builder`
 * Deprecate `GsonCodec`
diff --git a/README.md b/README.md
index 622d458153..1d43eacd3f 100644
--- a/README.md
+++ b/README.md
@@ -56,7 +56,7 @@ For further flexibility, you can use Dagger modules directly.  See the `Dagger`
 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.
 
-```
+```java
 static class ForwardedForInterceptor implements RequestInterceptor {
   @Override public void apply(RequestTemplate template) {
     template.header("X-Forwarded-For", "origin.host.com");
@@ -66,6 +66,12 @@ static class ForwardedForInterceptor implements RequestInterceptor {
 Bank bank = Feign.builder().decoder(accountDecoder).requestInterceptor(new ForwardedForInterceptor()).target(Bank.class, "https://api.examplebank.com");
 ```
 
+Another common example of an interceptor would be authentication, such as using the built-in `BasicAuthRequestInterceptor`.
+
+```java
+Bank bank = Feign.builder().decoder(accountDecoder).requestInterceptor(new BasicAuthRequestInterceptor(username, password)).target(Bank.class, "https://api.examplebank.com");
+```
+
 ### Multiple Interfaces
 Feign can produce multiple api interfaces.  These are defined as `Target` (default `HardCodedTarget`), which allow for dynamic discovery and decoration of requests prior to execution.
 
diff --git a/core/src/main/java/feign/Util.java b/core/src/main/java/feign/Util.java
index f3b7b0ac6f..0036be7bf0 100644
--- a/core/src/main/java/feign/Util.java
+++ b/core/src/main/java/feign/Util.java
@@ -60,6 +60,10 @@ private Util() { // no instances
    * UTF-8: eight-bit UCS Transformation Format.
    */
   public static final Charset UTF_8 = Charset.forName("UTF-8");
+  /**
+   * ISO-8859-1: ISO Latin Alphabet Number 1 (ISO-LATIN-1).
+   */
+  public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
 
   /**
    * Copy of {@code com.google.common.base.Preconditions#checkArgument}.
diff --git a/core/src/main/java/feign/auth/BasicAuthRequestInterceptor.java b/core/src/main/java/feign/auth/BasicAuthRequestInterceptor.java
new file mode 100644
index 0000000000..b0a2ee9ebf
--- /dev/null
+++ b/core/src/main/java/feign/auth/BasicAuthRequestInterceptor.java
@@ -0,0 +1,69 @@
+/*
+ * 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.auth;
+
+import feign.RequestInterceptor;
+import feign.RequestTemplate;
+import sun.misc.BASE64Encoder;
+
+import java.nio.charset.Charset;
+
+import static feign.Util.checkNotNull;
+import static feign.Util.ISO_8859_1;
+
+/**
+ * An interceptor that adds the request header needed to use HTTP basic authentication.
+ */
+public class BasicAuthRequestInterceptor implements RequestInterceptor {
+  private final String headerValue;
+
+  /**
+   * Creates an interceptor that authenticates all requests with the specified username and password encoded using
+   * ISO-8859-1.
+   *
+   * @param username the username to use for authentication
+   * @param password the password to use for authentication
+   */
+  public BasicAuthRequestInterceptor(String username, String password) {
+    this(username, password, ISO_8859_1);
+  }
+
+  /**
+   * Creates an interceptor that authenticates all requests with the specified username and password encoded using
+   * the specified charset.
+   *
+   * @param username the username to use for authentication
+   * @param password the password to use for authentication
+   * @param charset the charset to use when encoding the credentials
+   */
+  public BasicAuthRequestInterceptor(String username, String password, Charset charset) {
+    checkNotNull(username, "username");
+    checkNotNull(password, "password");
+    this.headerValue = "Basic " + base64Encode((username + ":" + password).getBytes(charset));
+  }
+
+  @Override public void apply(RequestTemplate template) {
+    template.header("Authorization", headerValue);
+  }
+
+  /*
+   * This uses a Sun internal method; if we ever encounter a case where this method is not available, the appropriate
+   * response would be to pull the necessary portions of Guava's BaseEncoding class into Util.
+   */
+  private static String base64Encode(byte[] bytes) {
+    return new BASE64Encoder().encode(bytes);
+  }
+}
diff --git a/core/src/test/java/feign/auth/BasicAuthRequestInterceptorTest.java b/core/src/test/java/feign/auth/BasicAuthRequestInterceptorTest.java
new file mode 100644
index 0000000000..3a8c6bf5a1
--- /dev/null
+++ b/core/src/test/java/feign/auth/BasicAuthRequestInterceptorTest.java
@@ -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.auth;
+
+import feign.RequestTemplate;
+import org.testng.annotations.Test;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import static org.testng.Assert.assertEquals;
+
+/**
+ * Tests for {@link BasicAuthRequestInterceptor}.
+ */
+public class BasicAuthRequestInterceptorTest {
+  /**
+   * Tests that request headers are added as expected.
+   */
+  @Test public void testAuthentication() {
+    RequestTemplate template = new RequestTemplate();
+    BasicAuthRequestInterceptor interceptor = new BasicAuthRequestInterceptor("Aladdin", "open sesame");
+    interceptor.apply(template);
+    Collection actualValue = template.headers().get("Authorization");
+    Collection expectedValue = Collections.singletonList("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==");
+    assertEquals(actualValue, expectedValue);
+  }
+}

From c92025c60817f0874bba8fbd85f356b0389fdf37 Mon Sep 17 00:00:00 2001
From: Matt Hurne 
Date: Tue, 15 Oct 2013 15:57:53 -0400
Subject: [PATCH 123/179] Squashed commit of the following:

commit 34eb5751c760cf1f11cdbab920d6a3a1c6f06640
Author: Matt Hurne 
Date:   Tue Oct 15 15:54:20 2013 -0400

    Remove unnecessary defensive close of Reader

commit 38e51606750517d4a52571c408190e614a4a4834
Author: Matt Hurne 
Date:   Tue Oct 8 13:59:35 2013 -0400

    Replace wildcard import with individual imports

commit cc845814ea677ba5920caf5a7a914010623caf1e
Author: Matt Hurne 
Date:   Tue Oct 8 13:55:37 2013 -0400

    Revert GitHub example to use JacksonDecoder rather than JacksonModule now that JacksonDecoder behaves sensibly with its default ObjectMapper

commit 8b9638261afe2549c3a43238ee1b66d044f969f4
Author: Matt Hurne 
Date:   Tue Oct 8 13:52:45 2013 -0400

    Configure default ObjectMapper used by JacksonEncoder and JacksonDecoder with sensible overrides of default behaviors

commit 0f275bf7574b66c20a0e6aefe0140f599638992f
Author: Matt Hurne 
Date:   Tue Oct 8 13:18:26 2013 -0400

    Unwrap RuntimeJsonMappingExceptions caught in JacksonDecoder, since they are only ever used to wrap JsonMappingExceptions, which are IOExceptions.

commit 1b6995260a5727796e388bbb0b6c88b65e182415
Author: Matt Hurne 
Date:   Tue Oct 8 13:09:44 2013 -0400

    Update Jackson integration README

commit add4007a59559e7b4e2accfa0b0a0215bab62cef
Author: Matt Hurne 
Date:   Tue Oct 8 13:07:35 2013 -0400

    Update CHANGES and README to reflect addition of Jackson integration

commit 86c0fcfc704b1b8d03e5eaf69c608fc2761d617b
Author: Matt Hurne 
Date:   Tue Oct 8 12:11:56 2013 -0400

    Update Jackson GitHub example to make use of JacksonModule, and to avoid the need for Jackson annotations

commit 1552b3f8239636da0f27ace3c7b42038536e5caf
Author: Matt Hurne 
Date:   Tue Oct 8 12:05:56 2013 -0400

    Replace wildcard import with individual imports

commit 0b7cfd08516dfbf66f1a69263ed456f2c0671c76
Author: Matt Hurne 
Date:   Tue Oct 8 11:01:11 2013 -0400

    Initial implementation of Jackson codec

    This new codec may be used as an alternative to Gson.

commit 94027ec3319f5145c0e18ef472d8e928e97a9527
Author: Matt Hurne 
Date:   Tue Oct 8 08:31:14 2013 -0400

    Improve EncodeException and DecodeException Javadoc comments
---
 CHANGES.md                                    |   1 +
 README.md                                     |  12 ++
 build.gradle                                  |  15 ++
 .../java/feign/codec/DecodeException.java     |   2 +-
 .../java/feign/codec/EncodeException.java     |   4 +-
 jackson/README.md                             |  33 ++++
 .../java/feign/jackson/JacksonDecoder.java    |  53 +++++
 .../java/feign/jackson/JacksonEncoder.java    |  46 +++++
 .../java/feign/jackson/JacksonModule.java     | 103 ++++++++++
 .../java/feign/jackson/JacksonModuleTest.java | 184 ++++++++++++++++++
 .../feign/jackson/examples/GitHubExample.java |  40 ++++
 settings.gradle                               |   2 +-
 12 files changed, 491 insertions(+), 4 deletions(-)
 create mode 100644 jackson/README.md
 create mode 100644 jackson/src/main/java/feign/jackson/JacksonDecoder.java
 create mode 100644 jackson/src/main/java/feign/jackson/JacksonEncoder.java
 create mode 100644 jackson/src/main/java/feign/jackson/JacksonModule.java
 create mode 100644 jackson/src/test/java/feign/jackson/JacksonModuleTest.java
 create mode 100644 jackson/src/test/java/feign/jackson/examples/GitHubExample.java

diff --git a/CHANGES.md b/CHANGES.md
index 833a455239..00f5e219f8 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,6 @@
 ### Version 5.4.0
 * Add `BasicAuthRequestInterceptor`
+* Add Jackson integration
 
 ### Version 5.3.0
 * Split `GsonCodec` into `GsonEncoder` and `GsonDecoder`, which are easy to use with `Feign.Builder`
diff --git a/README.md b/README.md
index 1d43eacd3f..e2a56ef1ed 100644
--- a/README.md
+++ b/README.md
@@ -99,6 +99,18 @@ GitHub github = Feign.builder()
                      .target(GitHub.class, "https://api.github.com");
 ```
 
+### Jackson
+[JacksonModule](https://github.com/Netflix/feign/tree/master/jackson) adds an encoder and decoder you can use with a JSON API.
+
+Add `JacksonEncoder` and/or `JacksonDecoder` to your `Feign.Builder` like so:
+
+```java
+GitHub github = Feign.builder()
+                     .encoder(new JacksonEncoder())
+                     .decoder(new JacksonDecoder())
+                     .target(GitHub.class, "https://api.github.com");
+```
+
 ### Sax
 [SaxDecoder](https://github.com/Netflix/feign/tree/master/sax) allows you to decode XML in a way that is compatible with normal JVM and also Android environments.
 
diff --git a/build.gradle b/build.gradle
index 022cce7e8c..3da4379f3c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -73,6 +73,21 @@ project(':feign-gson') {
     }
 }
 
+project(':feign-jackson') {
+    apply plugin: 'java'
+
+    test {
+        useTestNG()
+    }
+
+    dependencies {
+        compile     project(':feign-core')
+        compile     'com.fasterxml.jackson.core:jackson-databind:2.2.2'
+        testCompile 'org.testng:testng:6.8.5'
+        testCompile 'com.google.guava:guava:14.0.1'
+    }
+}
+
 project(':feign-jaxrs') {
     apply plugin: 'java'
 
diff --git a/core/src/main/java/feign/codec/DecodeException.java b/core/src/main/java/feign/codec/DecodeException.java
index 5efab25ba6..1671bbdb60 100644
--- a/core/src/main/java/feign/codec/DecodeException.java
+++ b/core/src/main/java/feign/codec/DecodeException.java
@@ -22,7 +22,7 @@
 /**
  * Similar to {@code javax.websocket.DecodeException}, raised when a problem
  * occurs decoding a message.  Note that {@code DecodeException} is not an
- * {@code IOException}, nor have one set as its cause.
+ * {@code IOException}, nor does it have one set as its cause.
  */
 public class DecodeException extends FeignException {
 
diff --git a/core/src/main/java/feign/codec/EncodeException.java b/core/src/main/java/feign/codec/EncodeException.java
index 12d06ba340..bc9c660ca0 100644
--- a/core/src/main/java/feign/codec/EncodeException.java
+++ b/core/src/main/java/feign/codec/EncodeException.java
@@ -21,8 +21,8 @@
 
 /**
  * Similar to {@code javax.websocket.EncodeException}, raised when a problem
- * occurs decoding a message.  Note that {@code DecodeException} is not an
- * {@code IOException}, nor have one set as its cause.
+ * occurs encoding a message.  Note that {@code EncodeException} is not an
+ * {@code IOException}, nor does it have one set as its cause.
  */
 public class EncodeException extends FeignException {
 
diff --git a/jackson/README.md b/jackson/README.md
new file mode 100644
index 0000000000..a6b8f0fcdc
--- /dev/null
+++ b/jackson/README.md
@@ -0,0 +1,33 @@
+Jackson Codec
+===================
+
+This module adds support for encoding and decoding JSON via Jackson.
+
+Add `JacksonEncoder` and/or `JacksonDecoder` to your `Feign.Builder` like so:
+
+```java
+GitHub github = Feign.builder()
+                     .encoder(new JacksonEncoder())
+                     .decoder(new JacksonDecoder())
+                     .target(GitHub.class, "https://api.github.com");
+```
+
+If you want to customize the `ObjectMapper` that is used, provide it to the `JacksonEncoder` and `JacksonDecoder`:
+
+```java
+ObjectMapper mapper = new ObjectMapper()
+        .setSerializationInclusion(JsonInclude.Include.NON_NULL)
+        .configure(SerializationFeature.INDENT_OUTPUT, true)
+        .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+
+GitHub github = Feign.builder()
+                     .encoder(new JacksonEncoder(mapper))
+                     .decoder(new JacksonDecoder(mapper))
+                     .target(GitHub.class, "https://api.github.com");
+```
+
+Alternatively, you can add the encoder and decoder to your Dagger object graph using the provided `JacksonModule` like so:
+
+```java
+GitHub github = Feign.create(GitHub.class, "https://api.github.com", new JacksonModule());
+```
diff --git a/jackson/src/main/java/feign/jackson/JacksonDecoder.java b/jackson/src/main/java/feign/jackson/JacksonDecoder.java
new file mode 100644
index 0000000000..83400afc14
--- /dev/null
+++ b/jackson/src/main/java/feign/jackson/JacksonDecoder.java
@@ -0,0 +1,53 @@
+/*
+ * 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.jackson;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.RuntimeJsonMappingException;
+import feign.Response;
+import feign.codec.Decoder;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.lang.reflect.Type;
+
+public class JacksonDecoder implements Decoder {
+  private final ObjectMapper mapper;
+
+  public JacksonDecoder() {
+    this(new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false));
+  }
+
+  public JacksonDecoder(ObjectMapper mapper) {
+    this.mapper = mapper;
+  }
+
+  @Override public Object decode(Response response, Type type) throws IOException {
+    if (response.body() == null) {
+      return null;
+    }
+    Reader reader = response.body().asReader();
+    try {
+      return mapper.readValue(reader, mapper.constructType(type));
+    } catch (RuntimeJsonMappingException e) {
+      if (e.getCause() != null && e.getCause() instanceof IOException) {
+        throw IOException.class.cast(e.getCause());
+      }
+      throw e;
+    }
+  }
+}
diff --git a/jackson/src/main/java/feign/jackson/JacksonEncoder.java b/jackson/src/main/java/feign/jackson/JacksonEncoder.java
new file mode 100644
index 0000000000..1cc6895f2b
--- /dev/null
+++ b/jackson/src/main/java/feign/jackson/JacksonEncoder.java
@@ -0,0 +1,46 @@
+/*
+ * 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.jackson;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import feign.RequestTemplate;
+import feign.codec.EncodeException;
+import feign.codec.Encoder;
+
+public class JacksonEncoder implements Encoder {
+  private final ObjectMapper mapper;
+
+  public JacksonEncoder() {
+    this(new ObjectMapper()
+        .setSerializationInclusion(JsonInclude.Include.NON_NULL)
+        .configure(SerializationFeature.INDENT_OUTPUT, true));
+  }
+
+  public JacksonEncoder(ObjectMapper mapper) {
+    this.mapper = mapper;
+  }
+
+  @Override public void encode(Object object, RequestTemplate template) throws EncodeException {
+    try {
+      template.body(mapper.writeValueAsString(object));
+    } catch (JsonProcessingException e) {
+      throw new EncodeException(e.getMessage(), e);
+    }
+  }
+}
diff --git a/jackson/src/main/java/feign/jackson/JacksonModule.java b/jackson/src/main/java/feign/jackson/JacksonModule.java
new file mode 100644
index 0000000000..7826118afa
--- /dev/null
+++ b/jackson/src/main/java/feign/jackson/JacksonModule.java
@@ -0,0 +1,103 @@
+/*
+ * 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.jackson;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.Module;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import dagger.Provides;
+import feign.Feign;
+import feign.codec.Decoder;
+import feign.codec.Encoder;
+
+import javax.inject.Singleton;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * 

Custom serializers/deserializers

+ *
+ * In order to specify custom json parsing, Jackson's {@code ObjectMapper} supports {@link JsonSerializer serializers} + * and {@link JsonDeserializer deserializers}, which can be bundled together in {@link Module modules}. + *

+ *
+ * Here's an example of adding a custom module. + *

+ *

+ * public class ObjectIdSerializer extends StdSerializer<ObjectId> {
+ *     public ObjectIdSerializer() {
+ *         super(ObjectId.class);
+ *     }
+ *
+ *     @Override
+ *     public void serialize(ObjectId value, JsonGenerator jsonGenerator, SerializerProvider provider) throws IOException {
+ *         jsonGenerator.writeString(value.toString());
+ *     }
+ * }
+ *
+ * public class ObjectIdDeserializer extends StdDeserializer<ObjectId> {
+ *     public ObjectIdDeserializer() {
+ *         super(ObjectId.class);
+ *     }
+ *
+ *     @Override
+ *     public ObjectId deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException {
+ *         return ObjectId.massageToObjectId(jsonParser.getValueAsString());
+ *     }
+ * }
+ *
+ * public class ObjectIdModule extends SimpleModule {
+ *     public ObjectIdModule() {
+ *         // first deserializers
+ *         addDeserializer(ObjectId.class, new ObjectIdDeserializer());
+ *
+ *         // then serializers:
+ *         addSerializer(ObjectId.class, new ObjectIdSerializer());
+ *     }
+ * }
+ *
+ * @Provides(type = Provides.Type.SET)
+ * Module objectIdModule() {
+ *     return new ObjectIdModule();
+ * }
+ * 
+ */ +@dagger.Module(injects = Feign.class, addsTo = Feign.Defaults.class) +public final class JacksonModule { + @Provides Encoder encoder(ObjectMapper mapper) { + return new JacksonEncoder(mapper); + } + + @Provides Decoder decoder(ObjectMapper mapper) { + return new JacksonDecoder(mapper); + } + + @Provides @Singleton ObjectMapper mapper(Set modules) { + return new ObjectMapper() + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .configure(SerializationFeature.INDENT_OUTPUT, true) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .registerModules(modules); + } + + @Provides(type = Provides.Type.SET_VALUES) Set noDefaultModules() { + return Collections.emptySet(); + } +} diff --git a/jackson/src/test/java/feign/jackson/JacksonModuleTest.java b/jackson/src/test/java/feign/jackson/JacksonModuleTest.java new file mode 100644 index 0000000000..c13583f6db --- /dev/null +++ b/jackson/src/test/java/feign/jackson/JacksonModuleTest.java @@ -0,0 +1,184 @@ +package feign.jackson; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.google.common.reflect.TypeToken; +import dagger.Module; +import dagger.ObjectGraph; +import dagger.Provides; +import feign.RequestTemplate; +import feign.Response; +import feign.codec.Decoder; +import feign.codec.Encoder; +import org.testng.annotations.Test; + +import javax.inject.Inject; +import java.io.IOException; +import java.util.*; + +import static org.testng.Assert.assertEquals; + +@Test +public class JacksonModuleTest { + @Module(includes = JacksonModule.class, injects = EncoderAndDecoderBindings.class) + static class EncoderAndDecoderBindings { + @Inject + Encoder encoder; + @Inject + Decoder decoder; + } + + @Test + public void providesEncoderDecoder() throws Exception { + EncoderAndDecoderBindings bindings = new EncoderAndDecoderBindings(); + ObjectGraph.create(bindings).inject(bindings); + + assertEquals(bindings.encoder.getClass(), JacksonEncoder.class); + assertEquals(bindings.decoder.getClass(), JacksonDecoder.class); + } + + @Module(includes = JacksonModule.class, injects = EncoderBindings.class) + static class EncoderBindings { + @Inject Encoder encoder; + } + + @Test public void encodesMapObjectNumericalValuesAsInteger() throws Exception { + EncoderBindings bindings = new EncoderBindings(); + ObjectGraph.create(bindings).inject(bindings); + + Map map = new LinkedHashMap(); + map.put("foo", 1); + + RequestTemplate template = new RequestTemplate(); + bindings.encoder.encode(map, template); + assertEquals(template.body(), ""// + + "{\n" // + + " \"foo\" : 1\n" // + + "}"); + } + + @Test public void encodesFormParams() throws Exception { + EncoderBindings bindings = new EncoderBindings(); + ObjectGraph.create(bindings).inject(bindings); + + Map form = new LinkedHashMap(); + form.put("foo", 1); + form.put("bar", Arrays.asList(2, 3)); + + RequestTemplate template = new RequestTemplate(); + bindings.encoder.encode(form, template); + assertEquals(template.body(), ""// + + "{\n" // + + " \"foo\" : 1,\n" // + + " \"bar\" : [ 2, 3 ]\n" // + + "}"); + } + + static class Zone extends LinkedHashMap { + Zone() { + // for reflective instantiation. + } + + Zone(String name) { + this(name, null); + } + + Zone(String name, String id) { + put("name", name); + if (id != null) { + put("id", id); + } + } + + private static final long serialVersionUID = 1L; + } + + @Module(includes = JacksonModule.class, injects = DecoderBindings.class) + static class DecoderBindings { + @Inject Decoder decoder; + } + + @Test public void decodes() throws Exception { + DecoderBindings bindings = new DecoderBindings(); + ObjectGraph.create(bindings).inject(bindings); + + List zones = new LinkedList(); + zones.add(new Zone("denominator.io.")); + zones.add(new Zone("denominator.io.", "ABCD")); + + Response response = Response.create(200, "OK", Collections.>emptyMap(), zonesJson); + assertEquals(bindings.decoder.decode(response, new TypeToken>() { + }.getType()), zones); + } + + @Test public void nullBodyDecodesToNull() throws Exception { + DecoderBindings bindings = new DecoderBindings(); + ObjectGraph.create(bindings).inject(bindings); + + Response response = Response.create(204, "OK", Collections.>emptyMap(), null); + assertEquals(bindings.decoder.decode(response, String.class), null); + } + + private String zonesJson = ""// + + "[\n"// + + " {\n"// + + " \"name\": \"denominator.io.\"\n"// + + " },\n"// + + " {\n"// + + " \"name\": \"denominator.io.\",\n"// + + " \"id\": \"ABCD\"\n"// + + " }\n"// + + "]\n"; + + static class ZoneDeserializer extends StdDeserializer { + public ZoneDeserializer() { + super(Zone.class); + } + + @Override + public Zone deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { + Zone zone = new Zone(); + jp.nextToken(); + while (jp.nextToken() != JsonToken.END_OBJECT) { + String name = jp.getCurrentName(); + String value = jp.getValueAsString(); + if (value != null) { + zone.put(name, value.toUpperCase()); + } + } + return zone; + } + } + + static class ZoneModule extends SimpleModule { + public ZoneModule() { + addDeserializer(Zone.class, new ZoneDeserializer()); + } + } + + @Module(includes = JacksonModule.class, injects = CustomJacksonModule.class) + static class CustomJacksonModule { + @Inject Decoder decoder; + + @Provides(type = Provides.Type.SET) + com.fasterxml.jackson.databind.Module upperZone() { + return new ZoneModule(); + } + } + + @Test public void customDecoder() throws Exception { + CustomJacksonModule bindings = new CustomJacksonModule(); + ObjectGraph.create(bindings).inject(bindings); + + List zones = new LinkedList(); + zones.add(new Zone("DENOMINATOR.IO.")); + zones.add(new Zone("DENOMINATOR.IO.", "ABCD")); + + Response response = Response.create(200, "OK", Collections.>emptyMap(), zonesJson); + assertEquals(bindings.decoder.decode(response, new TypeToken>() { + }.getType()), zones); + } +} diff --git a/jackson/src/test/java/feign/jackson/examples/GitHubExample.java b/jackson/src/test/java/feign/jackson/examples/GitHubExample.java new file mode 100644 index 0000000000..24f490efb3 --- /dev/null +++ b/jackson/src/test/java/feign/jackson/examples/GitHubExample.java @@ -0,0 +1,40 @@ +package feign.jackson.examples; + +import feign.Feign; +import feign.RequestLine; +import feign.jackson.JacksonDecoder; + +import javax.inject.Named; +import java.util.List; + +/** + * adapted from {@code com.example.retrofit.GitHubClient} + */ +public class GitHubExample { + interface GitHub { + @RequestLine("GET /repos/{owner}/{repo}/contributors") + List contributors(@Named("owner") String owner, @Named("repo") String repo); + } + + static class Contributor { + private String login; + private int contributions; + + void setLogin(String login) { + this.login = login; + } + + void setContributions(int contributions) { + this.contributions = contributions; + } + } + + public static void main(String... args) throws InterruptedException { + GitHub github = Feign.builder().decoder(new JacksonDecoder()).target(GitHub.class, "https://api.github.com"); + System.out.println("Let's fetch and print a list of the contributors to this library."); + List contributors = github.contributors("netflix", "feign"); + for (Contributor contributor : contributors) { + System.out.println(contributor.login + " (" + contributor.contributions + ")"); + } + } +} diff --git a/settings.gradle b/settings.gradle index b7b41a0482..8dac555cc9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,5 @@ rootProject.name='feign' -include 'core', 'sax', 'gson', 'jaxrs', 'ribbon', 'example-github', 'example-wikipedia' +include 'core', 'sax', 'gson', 'jackson', 'jaxrs', 'ribbon', 'example-github', 'example-wikipedia' rootProject.children.each { childProject -> childProject.name = 'feign-' + childProject.name From 91e4d8209a0023b33e34c374c78a7c388870053e Mon Sep 17 00:00:00 2001 From: "David M. Carr" Date: Mon, 14 Oct 2013 17:56:59 -0400 Subject: [PATCH 124/179] Support binary request/response bodies (#57) Request/Response/RequestTemplate are now fundamentally based on a byte[] body field. For Request/RequestTemplate, if a charset is provided, it can be treated as text. For many users of the library, the change should barely be noticeable, as the methods that were changed were mostly used internally. There were some non-backwards-compatible signature changes that require a major version bump, however. --- CHANGES.md | 3 + README.md | 17 +++- core/src/main/java/feign/Client.java | 8 +- core/src/main/java/feign/Logger.java | 38 +++----- core/src/main/java/feign/MethodHandler.java | 5 +- core/src/main/java/feign/Request.java | 30 ++++-- core/src/main/java/feign/RequestTemplate.java | 50 +++++++--- core/src/main/java/feign/Response.java | 92 ++++++++++++------- core/src/main/java/feign/Util.java | 51 ++++++++++ core/src/main/java/feign/codec/Decoder.java | 12 +++ core/src/main/java/feign/codec/Encoder.java | 4 +- .../test/java/feign/DefaultContractTest.java | 5 +- core/src/test/java/feign/FeignTest.java | 37 ++++++++ .../java/feign/codec/DefaultDecoderTest.java | 16 +++- .../java/feign/codec/DefaultEncoderTest.java | 9 ++ .../feign/codec/DefaultErrorDecoderTest.java | 3 +- .../test/java/feign/gson/GsonModuleTest.java | 35 ++++--- .../java/feign/jackson/JacksonDecoder.java | 6 +- .../java/feign/jackson/JacksonModuleTest.java | 12 ++- .../src/main/java/feign/ribbon/LBClient.java | 3 +- sax/src/main/java/feign/sax/SAXDecoder.java | 8 +- .../test/java/feign/sax/SAXDecoderTest.java | 4 +- .../sax/examples/AWSSignatureVersion4.java | 7 +- 23 files changed, 330 insertions(+), 125 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 00f5e219f8..88225348c0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,6 @@ +### Version 6.0 +* Support binary request and response bodies. + ### Version 5.4.0 * Add `BasicAuthRequestInterceptor` * Add Jackson integration diff --git a/README.md b/README.md index e2a56ef1ed..7339c831fd 100644 --- a/README.md +++ b/README.md @@ -144,9 +144,9 @@ MyService api = Feign.create(MyService.class, "https://myAppProd", new RibbonMod ### Decoders `Feign.builder()` allows you to specify additional configuration such as how to decode a response. -If any methods in your interface return types besides `Response`, `String` or `void`, you'll need to configure a `Decoder`. +If any methods in your interface return types besides `Response`, `String`, `byte[]` or `void`, you'll need to configure a non-default `Decoder`. -Here's how to configure json decoding (using the `feign-gson` extension): +Here's how to configure JSON decoding (using the `feign-gson` extension): ```java GitHub github = Feign.builder() @@ -154,6 +154,19 @@ GitHub github = Feign.builder() .target(GitHub.class, "https://api.github.com"); ``` +### Encoders +`Feign.builder()` allows you to specify additional configuration such as how to encode a request. + +If any methods in your interface use parameters types besides `String` or `byte[]`, you'll need to configure a non-default `Encoder`. + +Here's how to configure JSON encoding (using the `feign-gson` extension): + +```json +GitHub github = Feign.builder() + .encoder(new GsonEncoder()) + .target(GitHub.class, "https://api.github.com"); +``` + ### Advanced usage and Dagger #### Dagger Feign can be directly wired into Dagger which keeps things at compile time and Android friendly. As opposed to exposing builders for config, Feign intends users to embed their config in Dagger. diff --git a/core/src/main/java/feign/Client.java b/core/src/main/java/feign/Client.java index 64e97682b8..aab143daa1 100644 --- a/core/src/main/java/feign/Client.java +++ b/core/src/main/java/feign/Client.java @@ -17,9 +17,7 @@ import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.io.OutputStream; -import java.io.Reader; import java.net.HttpURLConnection; import java.net.URL; import java.util.Collection; @@ -39,7 +37,6 @@ import static feign.Util.CONTENT_ENCODING; import static feign.Util.CONTENT_LENGTH; import static feign.Util.ENCODING_GZIP; -import static feign.Util.UTF_8; /** * Submits HTTP {@link Request requests}. Implementations are expected to be @@ -113,7 +110,7 @@ HttpURLConnection convertAndSend(Request request, Options options) throws IOExce out = new GZIPOutputStream(out); } try { - out.write(request.body().getBytes(UTF_8)); + out.write(request.body()); } finally { try { out.close(); @@ -144,8 +141,7 @@ Response convertResponse(HttpURLConnection connection) throws IOException { } else { stream = connection.getInputStream(); } - Reader body = stream != null ? new InputStreamReader(stream, UTF_8) : null; - return Response.create(status, reason, headers, body, length); + return Response.create(status, reason, headers, stream, length); } } } diff --git a/core/src/main/java/feign/Logger.java b/core/src/main/java/feign/Logger.java index b07e206453..dd68d9a89f 100644 --- a/core/src/main/java/feign/Logger.java +++ b/core/src/main/java/feign/Logger.java @@ -15,17 +15,15 @@ */ 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; +import static feign.Util.decodeOrDefault; import static feign.Util.UTF_8; -import static feign.Util.ensureClosed; import static feign.Util.valuesOrEmpty; /** @@ -145,15 +143,16 @@ void logRequest(String configKey, Level logLevel, Request request) { } } - int bytes = 0; + int bodyLength = 0; if (request.body() != null) { - bytes = request.body().getBytes(UTF_8).length; + bodyLength = request.body().length; if (logLevel.ordinal() >= Level.FULL.ordinal()) { + String bodyText = request.charset() != null ? new String(request.body(), request.charset()) : null; log(configKey, ""); // CRLF - log(configKey, "%s", request.body()); + log(configKey, "%s", bodyText != null ? bodyText : "Binary data"); } } - log(configKey, "---> END HTTP (%s-byte body)", bytes); + log(configKey, "---> END HTTP (%s-byte body)", bodyLength); } } @@ -171,27 +170,20 @@ Response logAndRebufferResponse(String configKey, Level logLevel, Response respo } } + int bodyLength = 0; if (response.body() != null) { if (logLevel.ordinal() >= Level.FULL.ordinal()) { log(configKey, ""); // CRLF } - - BufferedReader reader = new BufferedReader(response.body().asReader()); - try { - StringBuilder buffered = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - buffered.append(line); - if (logLevel.ordinal() >= Level.FULL.ordinal()) { - log(configKey, "%s", line); - } - } - String bodyAsString = buffered.toString(); - log(configKey, "<--- END HTTP (%s-byte body)", bodyAsString.getBytes(UTF_8).length); - return Response.create(response.status(), response.reason(), response.headers(), bodyAsString); - } finally { - ensureClosed(reader); + byte[] bodyData = Util.toByteArray(response.body().asInputStream()); + bodyLength = bodyData.length; + if (logLevel.ordinal() >= Level.FULL.ordinal() && bodyLength > 0) { + log(configKey, "%s", decodeOrDefault(bodyData, UTF_8, "Binary data")); } + log(configKey, "<--- END HTTP (%s-byte body)", bodyLength); + return Response.create(response.status(), response.reason(), response.headers(), bodyData); + } else { + log(configKey, "<--- END HTTP (%s-byte body)", bodyLength); } } return response; diff --git a/core/src/main/java/feign/MethodHandler.java b/core/src/main/java/feign/MethodHandler.java index 864759ef39..141e644252 100644 --- a/core/src/main/java/feign/MethodHandler.java +++ b/core/src/main/java/feign/MethodHandler.java @@ -141,8 +141,9 @@ Object executeAndDecode(RequestTemplate template) throws Throwable { if (response.body() == null) { return response; } - String bodyString = Util.toString(response.body().asReader()); - return Response.create(response.status(), response.reason(), response.headers(), bodyString); + // Ensure the response body is disconnected + byte[] bodyData = Util.toByteArray(response.body().asInputStream()); + return Response.create(response.status(), response.reason(), response.headers(), bodyData); } else if (void.class == metadata.returnType()) { return null; } else { diff --git a/core/src/main/java/feign/Request.java b/core/src/main/java/feign/Request.java index b40c956a05..76d0f54f59 100644 --- a/core/src/main/java/feign/Request.java +++ b/core/src/main/java/feign/Request.java @@ -15,6 +15,7 @@ */ package feign; +import java.nio.charset.Charset; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; @@ -25,26 +26,23 @@ /** * An immutable request to an http server. - *
- *

Note
- *
- * Since {@link Feign} is designed for non-binary apis, and expectations are - * that any request can be replayed, we only support a String body. */ public final class Request { private final String method; private final String url; private final Map> headers; - private final String body; + private final byte[] body; + private final Charset charset; - Request(String method, String url, Map> headers, String body) { + Request(String method, String url, Map> headers, byte[] body, Charset charset) { this.method = checkNotNull(method, "method of %s", url); this.url = checkNotNull(url, "url"); LinkedHashMap> copyOf = new LinkedHashMap>(); copyOf.putAll(checkNotNull(headers, "headers of %s %s", method, url)); this.headers = Collections.unmodifiableMap(copyOf); this.body = body; // nullable + this.charset = charset; // nullable } /* Method to invoke on the server. */ @@ -62,8 +60,20 @@ public Map> headers() { return headers; } - /* If present, this is the replayable body to send to the server. */ - public String body() { + /** + * The character set with which the body is encoded, or null if unknown or not applicable. When this is + * present, you can use {@code new String(req.body(), req.charset())} to access the body as a String. + */ + public Charset charset() { + return charset; + } + + /** + * If present, this is the replayable body to send to the server. In some cases, this may be interpretable as text. + * + * @see #charset() + */ + public byte[] body() { return body; } @@ -110,7 +120,7 @@ public int readTimeoutMillis() { } } if (body != null) { - builder.append('\n').append(body); + builder.append('\n').append(charset != null ? new String(body, charset) : "Binary data"); } return builder.toString(); } diff --git a/core/src/main/java/feign/RequestTemplate.java b/core/src/main/java/feign/RequestTemplate.java index fc3f7bd138..6a3916593a 100644 --- a/core/src/main/java/feign/RequestTemplate.java +++ b/core/src/main/java/feign/RequestTemplate.java @@ -19,6 +19,7 @@ import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -54,7 +55,8 @@ public final class RequestTemplate implements Serializable { private StringBuilder url = new StringBuilder(); private final Map> queries = new LinkedHashMap>(); private final Map> headers = new LinkedHashMap>(); - private String body; + private transient Charset charset; + private byte[] body; private String bodyTemplate; public RequestTemplate() { @@ -68,6 +70,7 @@ public RequestTemplate(RequestTemplate toCopy) { this.url.append(toCopy.url); this.queries.putAll(toCopy.queries); this.headers.putAll(toCopy.headers); + this.charset = toCopy.charset; this.body = toCopy.body; this.bodyTemplate = toCopy.bodyTemplate; } @@ -117,7 +120,7 @@ public RequestTemplate resolve(Map unencoded) { /* roughly analogous to {@code javax.ws.rs.client.Target.request()}. */ public Request request() { return new Request(method, new StringBuilder(url).append(queryLine()).toString(), - headers, body); + headers, body, charset); } private static String urlDecode(String arg) { @@ -391,18 +394,39 @@ public Map> headers() { * * @see Request#body() */ - public RequestTemplate body(String body) { - this.body = body; - if (this.body != null) { - byte[] contentLength = body.getBytes(UTF_8); - header(CONTENT_LENGTH, String.valueOf(contentLength.length)); - } + public RequestTemplate body(byte[] bodyData, Charset charset) { this.bodyTemplate = null; + this.charset = charset; + this.body = bodyData; + int bodyLength = bodyData != null ? bodyData.length : 0; + header(CONTENT_LENGTH, String.valueOf(bodyLength)); return this; } - /* @see Request#body() */ - public String body() { + /** + * replaces the {@link feign.Util#CONTENT_LENGTH} header. + *
+ * Usually populated by an {@link feign.codec.Encoder}. + * + * @see Request#body() + */ + public RequestTemplate body(String bodyText) { + byte[] bodyData = bodyText != null ? bodyText.getBytes(UTF_8) : null; + return body(bodyData, UTF_8); + } + + /** + * The character set with which the body is encoded, or null if unknown or not applicable. When this is + * present, you can use {@code new String(req.body(), req.charset())} to access the body as a String. + */ + public Charset charset() { + return charset; + } + + /** + * @see Request#body() + */ + public byte[] body() { return body; } @@ -413,6 +437,7 @@ public String body() { */ public RequestTemplate bodyTemplate(String bodyTemplate) { this.bodyTemplate = bodyTemplate; + this.charset = null; this.body = null; return this; } @@ -426,10 +451,7 @@ public String bodyTemplate() { } /** - * if there are any query params in the {@link #body()}, this will extract - * them out. - * - * @return + * if there are any query params in the URL, this will extract them out. */ private StringBuilder pullAnyQueriesOutOfUrl(StringBuilder url) { // parse out queries diff --git a/core/src/main/java/feign/Response.java b/core/src/main/java/feign/Response.java index 6baa2b8426..2324254d74 100644 --- a/core/src/main/java/feign/Response.java +++ b/core/src/main/java/feign/Response.java @@ -15,16 +15,20 @@ */ package feign; +import java.io.ByteArrayInputStream; import java.io.Closeable; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.io.Reader; -import java.io.StringReader; +import java.nio.charset.Charset; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import static feign.Util.UTF_8; +import static feign.Util.decodeOrDefault; import static feign.Util.checkNotNull; import static feign.Util.checkState; import static feign.Util.valuesOrEmpty; @@ -40,12 +44,18 @@ public final class Response { private final Body body; public static Response create(int status, String reason, Map> headers, - Reader chars, Integer length) { - return new Response(status, reason, headers, ReaderBody.orNull(chars, length)); + InputStream inputStream, Integer length) { + return new Response(status, reason, headers, InputStreamBody.orNull(inputStream, length)); } - public static Response create(int status, String reason, Map> headers, String chars) { - return new Response(status, reason, headers, StringBody.orNull(chars)); + public static Response create(int status, String reason, Map> headers, + byte[] data) { + return new Response(status, reason, headers, ByteArrayBody.orNull(data)); + } + + public static Response create(int status, String reason, Map> headers, + String text, Charset charset) { + return new Response(status, reason, headers, ByteArrayBody.orNull(text, charset)); } private Response(int status, String reason, Map> headers, Body body) { @@ -94,28 +104,34 @@ public interface Body extends Closeable { Integer length(); /** - * True if {@link #asReader()} can be called more than once. + * True if {@link #asInputStream()} and {@link #asReader()} can be called more than once. */ boolean isRepeatable(); + /** + * It is the responsibility of the caller to close the stream. + */ + InputStream asInputStream() throws IOException; + /** * It is the responsibility of the caller to close the stream. */ Reader asReader() throws IOException; } - private static final class ReaderBody implements Response.Body { - private static Body orNull(Reader chars, Integer length) { - if (chars == null) + private static final class InputStreamBody implements Response.Body { + private static Body orNull(InputStream inputStream, Integer length) { + if (inputStream == null) { return null; - return new ReaderBody(chars, length); + } + return new InputStreamBody(inputStream, length); } - private final Reader chars; + private final InputStream inputStream; private final Integer length; - private ReaderBody(Reader chars, Integer length) { - this.chars = chars; + private InputStreamBody(InputStream inputStream, Integer length) { + this.inputStream = inputStream; this.length = length; } @@ -127,50 +143,62 @@ private ReaderBody(Reader chars, Integer length) { return false; } + @Override public InputStream asInputStream() throws IOException { + return inputStream; + } + @Override public Reader asReader() throws IOException { - return chars; + return new InputStreamReader(inputStream, UTF_8); } @Override public void close() throws IOException { - chars.close(); + inputStream.close(); } } - private static final class StringBody implements Response.Body { - private static Body orNull(String chars) { - if (chars == null) + private static final class ByteArrayBody implements Response.Body { + private static Body orNull(byte[] data) { + if (data == null) { return null; - return new StringBody(chars); + } + return new ByteArrayBody(data); } - private final String chars; - - public StringBody(String chars) { - this.chars = chars; + private static Body orNull(String text, Charset charset) { + if (text == null) { + return null; + } + checkNotNull(charset, "charset"); + return new ByteArrayBody(text.getBytes(charset)); } - private volatile Integer length; + private final byte[] data; + + public ByteArrayBody(byte[] data) { + this.data = data; + } @Override public Integer length() { - if (length == null) { - length = chars.getBytes(UTF_8).length; - } - return length; + return data.length; } @Override public boolean isRepeatable() { return true; } + @Override public InputStream asInputStream() throws IOException { + return new ByteArrayInputStream(data); + } + @Override public Reader asReader() throws IOException { - return new StringReader(chars); + return new InputStreamReader(asInputStream(), UTF_8); } - public String toString() { - return chars; + @Override public void close() throws IOException { } - @Override public void close() { + @Override public String toString() { + return decodeOrDefault(data, UTF_8, "Binary data"); } } diff --git a/core/src/main/java/feign/Util.java b/core/src/main/java/feign/Util.java index 0036be7bf0..2b847fa6c6 100644 --- a/core/src/main/java/feign/Util.java +++ b/core/src/main/java/feign/Util.java @@ -15,14 +15,19 @@ */ package feign; +import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.io.Reader; import java.lang.reflect.Array; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.WildcardType; +import java.nio.ByteBuffer; import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; @@ -192,4 +197,50 @@ public static String toString(Reader reader) throws IOException { ensureClosed(reader); } } + + /** + * Adapted from {@code com.google.common.io.ByteStreams.toByteArray()}. + */ + public static byte[] toByteArray(InputStream in) throws IOException { + checkNotNull(in, "in"); + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + copy(in, out); + return out.toByteArray(); + } finally { + ensureClosed(in); + } + } + + /** + * Adapted from {@code com.google.common.io.ByteStreams.copy()}. + */ + private static long copy(InputStream from, OutputStream to) + throws IOException { + checkNotNull(from, "from"); + checkNotNull(to, "to"); + byte[] buf = new byte[BUF_SIZE]; + long total = 0; + while (true) { + int r = from.read(buf); + if (r == -1) { + break; + } + to.write(buf, 0, r); + total += r; + } + return total; + } + + static String decodeOrDefault(byte[] data, Charset charset, String defaultValue) { + if (data == null) { + return defaultValue; + } + checkNotNull(charset, "charset"); + try { + return charset.newDecoder().decode(ByteBuffer.wrap(data)).toString(); + } catch (CharacterCodingException ex) { + return defaultValue; + } + } } diff --git a/core/src/main/java/feign/codec/Decoder.java b/core/src/main/java/feign/codec/Decoder.java index 8167854c26..346b149bfb 100644 --- a/core/src/main/java/feign/codec/Decoder.java +++ b/core/src/main/java/feign/codec/Decoder.java @@ -17,6 +17,7 @@ import feign.FeignException; import feign.Response; +import feign.Util; import java.io.IOException; import java.lang.reflect.Type; @@ -76,5 +77,16 @@ public interface Decoder { * Default implementation of {@code Decoder}. */ public class Default extends StringDecoder { + @Override + public Object decode(Response response, Type type) throws IOException { + Response.Body body = response.body(); + if (body == null) { + return null; + } + if (byte[].class.equals(type)) { + return Util.toByteArray(body.asInputStream()); + } + return super.decode(response, type); + } } } diff --git a/core/src/main/java/feign/codec/Encoder.java b/core/src/main/java/feign/codec/Encoder.java index ab7e39f8c7..c3b07d591a 100644 --- a/core/src/main/java/feign/codec/Encoder.java +++ b/core/src/main/java/feign/codec/Encoder.java @@ -69,13 +69,15 @@ public interface Encoder { void encode(Object object, RequestTemplate template) throws EncodeException; /** - * Default implementation of {@code Encoder} that supports {@code String}s only. + * Default implementation of {@code Encoder}. */ public class Default implements Encoder { @Override public void encode(Object object, RequestTemplate template) throws EncodeException { if (object instanceof String) { template.body(object.toString()); + } else if (object instanceof byte[]) { + template.body((byte[]) object, null); } else if (object != null) { throw new EncodeException(format("%s is not a type supported by this encoder.", object.getClass())); } diff --git a/core/src/test/java/feign/DefaultContractTest.java b/core/src/test/java/feign/DefaultContractTest.java index 7dae475857..e268fb7f77 100644 --- a/core/src/test/java/feign/DefaultContractTest.java +++ b/core/src/test/java/feign/DefaultContractTest.java @@ -29,6 +29,8 @@ import static org.testng.Assert.assertNull; import static org.testng.Assert.assertTrue; +import static feign.Util.UTF_8; + /** * Tests interfaces defined per {@link Contract.Default} are interpreted into expected {@link feign * .RequestTemplate template} @@ -154,8 +156,9 @@ interface BodyWithoutParameters { } @Test public void bodyWithoutParameters() throws Exception { + String expectedBody = ""; MethodMetadata md = contract.parseAndValidatateMetadata(BodyWithoutParameters.class.getDeclaredMethod("post")); - assertEquals(md.template().body(), ""); + assertEquals(md.template().body(), expectedBody.getBytes(UTF_8)); assertFalse(md.template().bodyTemplate() != null); assertTrue(md.formParams().isEmpty()); assertTrue(md.indexToName().isEmpty()); diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index fa86f19da5..801422e255 100644 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -113,6 +113,10 @@ public void iterableQueryParams() throws IOException, InterruptedException { interface OtherTestInterface { @RequestLine("POST /") String post(); + + @RequestLine("POST /") byte[] binaryResponseBody(); + + @RequestLine("POST /") void binaryRequestBody(byte[] contents); } @Module(library = true, overrides = true) @@ -499,4 +503,37 @@ static class DisableHostnameVerification { assertEquals(i1.hashCode(), i1.hashCode()); assertEquals(i1.hashCode(), i2.hashCode()); } + + @Test public void decodeLogicSupportsByteArray() throws Exception { + byte[] expectedResponse = {12, 34, 56}; + final MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse().setBody(expectedResponse)); + server.play(); + + try { + OtherTestInterface api = Feign.builder().target(OtherTestInterface.class, "http://localhost:" + server.getPort()); + + byte[] actualResponse = api.binaryResponseBody(); + assertEquals(actualResponse, expectedResponse); + } finally { + server.shutdown(); + } + } + + @Test public void encodeLogicSupportsByteArray() throws Exception { + byte[] expectedRequest = {12, 34, 56}; + final MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse()); + server.play(); + + try { + OtherTestInterface api = Feign.builder().target(OtherTestInterface.class, "http://localhost:" + server.getPort()); + + api.binaryRequestBody(expectedRequest); + byte[] actualRequest = server.takeRequest().getBody(); + assertEquals(actualRequest, expectedRequest); + } finally { + server.shutdown(); + } + } } diff --git a/core/src/test/java/feign/codec/DefaultDecoderTest.java b/core/src/test/java/feign/codec/DefaultDecoderTest.java index c5b58f8688..e270df5b53 100644 --- a/core/src/test/java/feign/codec/DefaultDecoderTest.java +++ b/core/src/test/java/feign/codec/DefaultDecoderTest.java @@ -19,7 +19,8 @@ import org.testng.annotations.Test; import org.w3c.dom.Document; -import java.io.StringReader; +import java.io.ByteArrayInputStream; +import java.io.InputStream; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -28,6 +29,8 @@ import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNull; +import static feign.Util.UTF_8; + public class DefaultDecoderTest { private final Decoder decoder = new Decoder.Default(); @@ -38,6 +41,13 @@ public class DefaultDecoderTest { assertEquals(decodedObject.toString(), "response body"); } + @Test public void testDecodesToByteArray() throws Exception { + Response response = knownResponse(); + Object decodedObject = decoder.decode(response, byte[].class); + assertEquals(decodedObject.getClass(), byte[].class); + assertEquals((byte[]) decodedObject, "response body".getBytes(UTF_8)); + } + @Test public void testDecodesNullBodyToNull() throws Exception { assertNull(decoder.decode(nullBodyResponse(), Document.class)); } @@ -49,10 +59,10 @@ public void testRefusesToDecodeOtherTypes() throws Exception { private Response knownResponse() { String content = "response body"; - StringReader reader = new StringReader(content); + InputStream inputStream = new ByteArrayInputStream(content.getBytes(UTF_8)); Map> headers = new HashMap>(); headers.put("Content-Type", Collections.singleton("text/plain")); - return Response.create(200, "OK", headers, reader, content.length()); + return Response.create(200, "OK", headers, inputStream, content.length()); } private Response nullBodyResponse() { diff --git a/core/src/test/java/feign/codec/DefaultEncoderTest.java b/core/src/test/java/feign/codec/DefaultEncoderTest.java index 21f93026fa..1dc4fe5985 100644 --- a/core/src/test/java/feign/codec/DefaultEncoderTest.java +++ b/core/src/test/java/feign/codec/DefaultEncoderTest.java @@ -22,6 +22,8 @@ import static org.testng.Assert.assertEquals; +import static feign.Util.UTF_8; + public class DefaultEncoderTest { private final Encoder encoder = new Encoder.Default(); @@ -29,6 +31,13 @@ public class DefaultEncoderTest { String content = "This is my content"; RequestTemplate template = new RequestTemplate(); encoder.encode(content, template); + assertEquals(template.body(), content.getBytes(UTF_8)); + } + + @Test public void testEncodesByteArray() throws Exception { + byte[] content = {12, 34, 56}; + RequestTemplate template = new RequestTemplate(); + encoder.encode(content, template); assertEquals(template.body(), content); } diff --git a/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java b/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java index efab2c9a76..e6173bca6c 100644 --- a/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java +++ b/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java @@ -27,6 +27,7 @@ import feign.RetryableException; import static feign.Util.RETRY_AFTER; +import static feign.Util.UTF_8; public class DefaultErrorDecoderTest { ErrorDecoder errorDecoder = new ErrorDecoder.Default(); @@ -42,7 +43,7 @@ public void throwsFeignException() throws Throwable { @Test(expectedExceptions = FeignException.class, expectedExceptionsMessageRegExp = "status 500 reading Service#foo\\(\\); content:\nhello world") public void throwsFeignExceptionIncludingBody() throws Throwable { Response response = Response.create(500, "Internal server error", ImmutableMap.>of(), - "hello world"); + "hello world", UTF_8); throw errorDecoder.decode("Service#foo()", response); } diff --git a/gson/src/test/java/feign/gson/GsonModuleTest.java b/gson/src/test/java/feign/gson/GsonModuleTest.java index 2c94476105..d0bce2abfc 100644 --- a/gson/src/test/java/feign/gson/GsonModuleTest.java +++ b/gson/src/test/java/feign/gson/GsonModuleTest.java @@ -40,6 +40,8 @@ import static org.testng.Assert.assertEquals; +import static feign.Util.UTF_8; + @Test public class GsonModuleTest { @Module(includes = GsonModule.class, injects = EncoderAndDecoderBindings.class) @@ -62,6 +64,11 @@ static class EncoderBindings { } @Test public void encodesMapObjectNumericalValuesAsInteger() throws Exception { + String expectedBody = "" + + "{\n" + + " \"foo\": 1\n" + + "}"; + EncoderBindings bindings = new EncoderBindings(); ObjectGraph.create(bindings).inject(bindings); @@ -70,13 +77,18 @@ static class EncoderBindings { RequestTemplate template = new RequestTemplate(); bindings.encoder.encode(map, template); - assertEquals(template.body(), ""// - + "{\n" // - + " \"foo\": 1\n" // - + "}"); + assertEquals(template.body(), expectedBody.getBytes(UTF_8)); } @Test public void encodesFormParams() throws Exception { + String expectedBody = ""// + + "{\n" // + + " \"foo\": 1,\n" // + + " \"bar\": [\n" // + + " 2,\n" // + + " 3\n" // + + " ]\n" // + + "}"; EncoderBindings bindings = new EncoderBindings(); ObjectGraph.create(bindings).inject(bindings); @@ -87,14 +99,7 @@ static class EncoderBindings { RequestTemplate template = new RequestTemplate(); bindings.encoder.encode(form, template); - assertEquals(template.body(), ""// - + "{\n" // - + " \"foo\": 1,\n" // - + " \"bar\": [\n" // - + " 2,\n" // - + " 3\n" // - + " ]\n" // - + "}"); + assertEquals(template.body(), expectedBody.getBytes(UTF_8)); } static class Zone extends LinkedHashMap { @@ -128,7 +133,8 @@ static class DecoderBindings { zones.add(new Zone("denominator.io.")); zones.add(new Zone("denominator.io.", "ABCD")); - Response response = Response.create(200, "OK", Collections.>emptyMap(), zonesJson); + Response response = + Response.create(200, "OK", Collections.>emptyMap(), zonesJson, UTF_8); assertEquals(bindings.decoder.decode(response, new TypeToken>() { }.getType()), zones); } @@ -184,7 +190,8 @@ static class CustomTypeAdapter { zones.add(new Zone("DENOMINATOR.IO.")); zones.add(new Zone("DENOMINATOR.IO.", "ABCD")); - Response response = Response.create(200, "OK", Collections.>emptyMap(), zonesJson); + Response response = + Response.create(200, "OK", Collections.>emptyMap(), zonesJson, UTF_8); assertEquals(bindings.decoder.decode(response, new TypeToken>() { }.getType()), zones); } diff --git a/jackson/src/main/java/feign/jackson/JacksonDecoder.java b/jackson/src/main/java/feign/jackson/JacksonDecoder.java index 83400afc14..f0734d3768 100644 --- a/jackson/src/main/java/feign/jackson/JacksonDecoder.java +++ b/jackson/src/main/java/feign/jackson/JacksonDecoder.java @@ -22,7 +22,7 @@ import feign.codec.Decoder; import java.io.IOException; -import java.io.Reader; +import java.io.InputStream; import java.lang.reflect.Type; public class JacksonDecoder implements Decoder { @@ -40,9 +40,9 @@ public JacksonDecoder(ObjectMapper mapper) { if (response.body() == null) { return null; } - Reader reader = response.body().asReader(); + InputStream inputStream = response.body().asInputStream(); try { - return mapper.readValue(reader, mapper.constructType(type)); + return mapper.readValue(inputStream, mapper.constructType(type)); } catch (RuntimeJsonMappingException e) { if (e.getCause() != null && e.getCause() instanceof IOException) { throw IOException.class.cast(e.getCause()); diff --git a/jackson/src/test/java/feign/jackson/JacksonModuleTest.java b/jackson/src/test/java/feign/jackson/JacksonModuleTest.java index c13583f6db..a4f9dfa8ef 100644 --- a/jackson/src/test/java/feign/jackson/JacksonModuleTest.java +++ b/jackson/src/test/java/feign/jackson/JacksonModuleTest.java @@ -21,6 +21,8 @@ import static org.testng.Assert.assertEquals; +import static feign.Util.UTF_8; + @Test public class JacksonModuleTest { @Module(includes = JacksonModule.class, injects = EncoderAndDecoderBindings.class) @@ -54,7 +56,7 @@ static class EncoderBindings { RequestTemplate template = new RequestTemplate(); bindings.encoder.encode(map, template); - assertEquals(template.body(), ""// + assertEquals(new String(template.body(), UTF_8), ""// + "{\n" // + " \"foo\" : 1\n" // + "}"); @@ -70,7 +72,7 @@ static class EncoderBindings { RequestTemplate template = new RequestTemplate(); bindings.encoder.encode(form, template); - assertEquals(template.body(), ""// + assertEquals(new String(template.body(), UTF_8), ""// + "{\n" // + " \"foo\" : 1,\n" // + " \"bar\" : [ 2, 3 ]\n" // @@ -109,7 +111,8 @@ static class DecoderBindings { zones.add(new Zone("denominator.io.")); zones.add(new Zone("denominator.io.", "ABCD")); - Response response = Response.create(200, "OK", Collections.>emptyMap(), zonesJson); + Response response = + Response.create(200, "OK", Collections.>emptyMap(), zonesJson, UTF_8); assertEquals(bindings.decoder.decode(response, new TypeToken>() { }.getType()), zones); } @@ -177,7 +180,8 @@ com.fasterxml.jackson.databind.Module upperZone() { zones.add(new Zone("DENOMINATOR.IO.")); zones.add(new Zone("DENOMINATOR.IO.", "ABCD")); - Response response = Response.create(200, "OK", Collections.>emptyMap(), zonesJson); + Response response = + Response.create(200, "OK", Collections.>emptyMap(), zonesJson, UTF_8); assertEquals(bindings.decoder.decode(response, new TypeToken>() { }.getType()), zones); } diff --git a/ribbon/src/main/java/feign/ribbon/LBClient.java b/ribbon/src/main/java/feign/ribbon/LBClient.java index 134c289bf4..a6d79205a2 100644 --- a/ribbon/src/main/java/feign/ribbon/LBClient.java +++ b/ribbon/src/main/java/feign/ribbon/LBClient.java @@ -93,7 +93,8 @@ Request toRequest() { .method(request.method()) .append(getUri().toASCIIString()) .headers(request.headers()) - .body(request.body()).request(); + .body(request.body(), request.charset()) + .request(); } public Object clone() { diff --git a/sax/src/main/java/feign/sax/SAXDecoder.java b/sax/src/main/java/feign/sax/SAXDecoder.java index 17981d734f..0afc817737 100644 --- a/sax/src/main/java/feign/sax/SAXDecoder.java +++ b/sax/src/main/java/feign/sax/SAXDecoder.java @@ -26,7 +26,7 @@ import javax.inject.Provider; import java.io.IOException; -import java.io.Reader; +import java.io.InputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Type; import java.util.LinkedHashMap; @@ -154,11 +154,11 @@ public Object decode(Response response, Type type) throws IOException, DecodeExc xmlReader.setFeature("http://xml.org/sax/features/namespaces", false); xmlReader.setFeature("http://xml.org/sax/features/validation", false); xmlReader.setContentHandler(handler); - Reader reader = response.body().asReader(); + InputStream inputStream = response.body().asInputStream(); try { - xmlReader.parse(new InputSource(reader)); + xmlReader.parse(new InputSource(inputStream)); } finally { - ensureClosed(reader); + ensureClosed(inputStream); } return handler.result(); } catch (SAXException e) { diff --git a/sax/src/test/java/feign/sax/SAXDecoderTest.java b/sax/src/test/java/feign/sax/SAXDecoderTest.java index d10fd4fe9a..c4b9abf07c 100644 --- a/sax/src/test/java/feign/sax/SAXDecoderTest.java +++ b/sax/src/test/java/feign/sax/SAXDecoderTest.java @@ -32,6 +32,8 @@ import static org.testng.Assert.assertEquals; +import static feign.Util.UTF_8; + // unbound wildcards are not currently injectable in dagger. @SuppressWarnings("rawtypes") public class SAXDecoderTest { @@ -64,7 +66,7 @@ public void niceErrorOnUnconfiguredType() throws ParseException, IOException { } private Response statusFailedResponse() { - return Response.create(200, "OK", Collections.>emptyMap(), statusFailed); + return Response.create(200, "OK", Collections.>emptyMap(), statusFailed, UTF_8); } static String statusFailed = ""// diff --git a/sax/src/test/java/feign/sax/examples/AWSSignatureVersion4.java b/sax/src/test/java/feign/sax/examples/AWSSignatureVersion4.java index 282be5f786..c229587b8c 100644 --- a/sax/src/test/java/feign/sax/examples/AWSSignatureVersion4.java +++ b/sax/src/test/java/feign/sax/examples/AWSSignatureVersion4.java @@ -128,9 +128,10 @@ private String canonicalString(RequestTemplate input, Multimap s canonicalRequest.append(Joiner.on(',').join(sortedLowercaseHeaders.keySet())).append('\n'); // HexEncode(Hash(Payload)) - if (input.body() != null) { - canonicalRequest.append(base16().lowerCase().encode( - sha256().hashString(input.body() != null ? input.body() : "", UTF_8).asBytes())); + String bodyText = + input.charset() != null && input.body() != null ? new String(input.body(), input.charset()) : null; + if (bodyText != null) { + canonicalRequest.append(base16().lowerCase().encode(sha256().hashString(bodyText, UTF_8).asBytes())); } else { canonicalRequest.append(EMPTY_STRING_HASH); } From 710d4774499fa7523d2d77d8713738700f921fb5 Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Wed, 6 Nov 2013 13:37:42 +0100 Subject: [PATCH 125/179] 7.0.0-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index a2f0b2797c..868bb9b9a2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=6.0.0-SNAPSHOT +version=7.0.0-SNAPSHOT From bc5011b91e937553664c6e38371121edf62258ed Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Thu, 14 Nov 2013 12:11:01 -0800 Subject: [PATCH 126/179] CHANGES update for last commit --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 88225348c0..f49a686b6b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ ### Version 6.0 * Support binary request and response bodies. +* Don't throw http status code exceptions when return type is `Response`. ### Version 5.4.0 * Add `BasicAuthRequestInterceptor` From aca25eb33131153bdcc2c8c3830f84ed87121ff0 Mon Sep 17 00:00:00 2001 From: Rodrigo Saito Date: Mon, 2 Dec 2013 21:49:25 -0200 Subject: [PATCH 127/179] When User and/or Password are too long, then the Authorization Header is broken because of sun Base64Encoder impl. Changed for another Base64 implementation --- CHANGES.md | 3 + core/src/main/java/feign/auth/Base64.java | 160 ++++++++++++++++++ .../auth/BasicAuthRequestInterceptor.java | 4 +- .../auth/BasicAuthRequestInterceptorTest.java | 14 ++ 4 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 core/src/main/java/feign/auth/Base64.java diff --git a/CHANGES.md b/CHANGES.md index f49a686b6b..18d2d4742c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,6 @@ +### Version 6.0.2 +* Fix for BasicAuthRequestInterceptor when username and/or password are long. + ### Version 6.0 * Support binary request and response bodies. * Don't throw http status code exceptions when return type is `Response`. diff --git a/core/src/main/java/feign/auth/Base64.java b/core/src/main/java/feign/auth/Base64.java new file mode 100644 index 0000000000..f75c092faf --- /dev/null +++ b/core/src/main/java/feign/auth/Base64.java @@ -0,0 +1,160 @@ +/* + * 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.auth; + +import java.io.UnsupportedEncodingException; + +/** + * copied from okhttp + * @author Alexander Y. Kleymenov + */ +final class Base64 { + + public static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + + private Base64() { + } + + public static byte[] decode(byte[] in) { + return decode(in, in.length); + } + + public static byte[] decode(byte[] in, int len) { + // approximate output length + int length = len / 4 * 3; + // return an empty array on empty or short input without padding + if (length == 0) { + return EMPTY_BYTE_ARRAY; + } + // temporary array + byte[] out = new byte[length]; + // number of padding characters ('=') + int pad = 0; + byte chr; + // compute the number of the padding characters + // and adjust the length of the input + for (; ; len--) { + chr = in[len - 1]; + // skip the neutral characters + if ((chr == '\n') || (chr == '\r') || (chr == ' ') || (chr == '\t')) { + continue; + } + if (chr == '=') { + pad++; + } else { + break; + } + } + // index in the output array + int outIndex = 0; + // index in the input array + int inIndex = 0; + // holds the value of the input character + int bits = 0; + // holds the value of the input quantum + int quantum = 0; + for (int i = 0; i < len; i++) { + chr = in[i]; + // skip the neutral characters + if ((chr == '\n') || (chr == '\r') || (chr == ' ') || (chr == '\t')) { + continue; + } + if ((chr >= 'A') && (chr <= 'Z')) { + // char ASCII value + // A 65 0 + // Z 90 25 (ASCII - 65) + bits = chr - 65; + } else if ((chr >= 'a') && (chr <= 'z')) { + // char ASCII value + // a 97 26 + // z 122 51 (ASCII - 71) + bits = chr - 71; + } else if ((chr >= '0') && (chr <= '9')) { + // char ASCII value + // 0 48 52 + // 9 57 61 (ASCII + 4) + bits = chr + 4; + } else if (chr == '+') { + bits = 62; + } else if (chr == '/') { + bits = 63; + } else { + return null; + } + // append the value to the quantum + quantum = (quantum << 6) | (byte) bits; + if (inIndex % 4 == 3) { + // 4 characters were read, so make the output: + out[outIndex++] = (byte) (quantum >> 16); + out[outIndex++] = (byte) (quantum >> 8); + out[outIndex++] = (byte) quantum; + } + inIndex++; + } + if (pad > 0) { + // adjust the quantum value according to the padding + quantum = quantum << (6 * pad); + // make output + out[outIndex++] = (byte) (quantum >> 16); + if (pad == 1) { + out[outIndex++] = (byte) (quantum >> 8); + } + } + // create the resulting array + byte[] result = new byte[outIndex]; + System.arraycopy(out, 0, result, 0, outIndex); + return result; + } + + private static final byte[] MAP = new byte[] { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', + 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', + 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', + '5', '6', '7', '8', '9', '+', '/' + }; + + public static String encode(byte[] in) { + int length = (in.length + 2) * 4 / 3; + byte[] out = new byte[length]; + int index = 0, end = in.length - in.length % 3; + for (int i = 0; i < end; i += 3) { + out[index++] = MAP[(in[i] & 0xff) >> 2]; + out[index++] = MAP[((in[i] & 0x03) << 4) | ((in[i + 1] & 0xff) >> 4)]; + out[index++] = MAP[((in[i + 1] & 0x0f) << 2) | ((in[i + 2] & 0xff) >> 6)]; + out[index++] = MAP[(in[i + 2] & 0x3f)]; + } + switch (in.length % 3) { + case 1: + out[index++] = MAP[(in[end] & 0xff) >> 2]; + out[index++] = MAP[(in[end] & 0x03) << 4]; + out[index++] = '='; + out[index++] = '='; + break; + case 2: + out[index++] = MAP[(in[end] & 0xff) >> 2]; + out[index++] = MAP[((in[end] & 0x03) << 4) | ((in[end + 1] & 0xff) >> 4)]; + out[index++] = MAP[((in[end + 1] & 0x0f) << 2)]; + out[index++] = '='; + break; + } + try { + return new String(out, 0, index, "US-ASCII"); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(e); + } + } +} + diff --git a/core/src/main/java/feign/auth/BasicAuthRequestInterceptor.java b/core/src/main/java/feign/auth/BasicAuthRequestInterceptor.java index b0a2ee9ebf..318f36f117 100644 --- a/core/src/main/java/feign/auth/BasicAuthRequestInterceptor.java +++ b/core/src/main/java/feign/auth/BasicAuthRequestInterceptor.java @@ -17,7 +17,6 @@ import feign.RequestInterceptor; import feign.RequestTemplate; -import sun.misc.BASE64Encoder; import java.nio.charset.Charset; @@ -64,6 +63,7 @@ public BasicAuthRequestInterceptor(String username, String password, Charset cha * response would be to pull the necessary portions of Guava's BaseEncoding class into Util. */ private static String base64Encode(byte[] bytes) { - return new BASE64Encoder().encode(bytes); + return Base64.encode(bytes); } } + diff --git a/core/src/test/java/feign/auth/BasicAuthRequestInterceptorTest.java b/core/src/test/java/feign/auth/BasicAuthRequestInterceptorTest.java index 3a8c6bf5a1..9b16527620 100644 --- a/core/src/test/java/feign/auth/BasicAuthRequestInterceptorTest.java +++ b/core/src/test/java/feign/auth/BasicAuthRequestInterceptorTest.java @@ -38,4 +38,18 @@ public class BasicAuthRequestInterceptorTest { Collection expectedValue = Collections.singletonList("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="); assertEquals(actualValue, expectedValue); } + + /** + * Tests that requests headers are added as expected when user and pass are too long + */ + @Test public void testAuthenticationWhenUserPassAreTooLong() { + RequestTemplate template = new RequestTemplate(); + BasicAuthRequestInterceptor interceptor = new BasicAuthRequestInterceptor("IOIOIOIOIOIOIOIOIOIOIOIOIOIOIOIOIOIOIO", + "101010101010101010101010101010101010101010"); + interceptor.apply(template); + Collection actualValue = template.headers().get("Authorization"); + Collection expectedValue = Collections. + singletonList("Basic SU9JT0lPSU9JT0lPSU9JT0lPSU9JT0lPSU9JT0lPSU9JT0lPSU86MTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEw"); + assertEquals(actualValue, expectedValue); + } } From addc58375d58995a6967e39e8a8da2b076030b36 Mon Sep 17 00:00:00 2001 From: "David M. Carr" Date: Fri, 27 Dec 2013 11:04:43 -0500 Subject: [PATCH 128/179] fix changelog for 6.0.1 (#95) It looks like the changes for 6.0.1 accidentally got marked as 6.0.2 in the changelog. --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 18d2d4742c..d81b0e626e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,4 @@ -### Version 6.0.2 +### Version 6.0.1 * Fix for BasicAuthRequestInterceptor when username and/or password are long. ### Version 6.0 From 2c5d359ce43d072129f5af73a77f29405c6af526 Mon Sep 17 00:00:00 2001 From: "David M. Carr" Date: Fri, 27 Dec 2013 11:01:44 -0500 Subject: [PATCH 129/179] slf4j: add slf4j integration module (#94) Adds a new "slf4j" module. A few methods in Logger are now protected rather than package protected to allow access by Logger subclasses that aren't inner classes of Logger. --- CHANGES.md | 3 + README.md | 13 +++ build.gradle | 15 +++ core/src/main/java/feign/Logger.java | 19 ++- settings.gradle | 2 +- slf4j/README.md | 12 ++ .../main/java/feign/slf4j/Slf4jLogger.java | 66 +++++++++++ .../test/java/feign/slf4j/ReflectionUtil.java | 37 ++++++ .../java/feign/slf4j/SimpleLoggerUtil.java | 47 ++++++++ .../java/feign/slf4j/Slf4jLoggerTest.java | 109 ++++++++++++++++++ 10 files changed, 311 insertions(+), 12 deletions(-) create mode 100644 slf4j/README.md create mode 100644 slf4j/src/main/java/feign/slf4j/Slf4jLogger.java create mode 100644 slf4j/src/test/java/feign/slf4j/ReflectionUtil.java create mode 100644 slf4j/src/test/java/feign/slf4j/SimpleLoggerUtil.java create mode 100644 slf4j/src/test/java/feign/slf4j/Slf4jLoggerTest.java diff --git a/CHANGES.md b/CHANGES.md index d81b0e626e..8c67a296b6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,6 @@ +### Version 6.1.0 +* Add [SLF4J](http://www.slf4j.org/) integration + ### Version 6.0.1 * Fix for BasicAuthRequestInterceptor when username and/or password are long. diff --git a/README.md b/README.md index 7339c831fd..281f5ab156 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,17 @@ Integration requires you to pass your ribbon client name as the host part of the MyService api = Feign.create(MyService.class, "https://myAppProd", new RibbonModule()); ``` +### SLF4J +[SLF4JModule](https://github.com/Netflix/feign/tree/master/slf4j) allows directing Feign's logging to [SLF4J](http://www.slf4j.org/), allowing you to easily use a logging backend of your choice (Logback, Log4J, etc.) + +To use SLF4J with Feign, add both the SLF4J module and an SLF4J binding of your choice to your classpath. Then, configure Feign to use the Slf4jLogger: + +```java +GitHub github = Feign.builder() + .logger(new Slf4jLogger()) + .target(GitHub.class, "https://api.github.com"); +``` + ### Decoders `Feign.builder()` allows you to specify additional configuration such as how to decode a response. @@ -198,3 +209,5 @@ GitHub github = Feign.builder() .logLevel(Logger.Level.FULL) .target(GitHub.class, "https://api.github.com"); ``` + +The SLF4JModule (see above) may also be of interest. diff --git a/build.gradle b/build.gradle index 3da4379f3c..319ed7f9bc 100644 --- a/build.gradle +++ b/build.gradle @@ -118,3 +118,18 @@ project(':feign-ribbon') { testCompile 'com.google.mockwebserver:mockwebserver:20130706' } } + +project(':feign-slf4j') { + apply plugin: 'java' + + test { + useTestNG() + } + + dependencies { + compile project(':feign-core') + compile 'org.slf4j:slf4j-api:1.7.5' + testCompile 'org.testng:testng:6.8.5' + testCompile 'org.slf4j:slf4j-simple:1.7.5' + } +} diff --git a/core/src/main/java/feign/Logger.java b/core/src/main/java/feign/Logger.java index dd68d9a89f..c693f68eb1 100644 --- a/core/src/main/java/feign/Logger.java +++ b/core/src/main/java/feign/Logger.java @@ -70,14 +70,13 @@ public static class ErrorLogger extends Logger { public static class JavaLogger extends Logger { final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(Logger.class.getName()); - @Override void logRequest(String configKey, Level logLevel, Request request) { + @Override protected void logRequest(String configKey, Level logLevel, Request request) { if (logger.isLoggable(java.util.logging.Level.FINE)) { super.logRequest(configKey, logLevel, request); } } - @Override - Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime) throws IOException { + @Override protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime) throws IOException { if (logger.isLoggable(java.util.logging.Level.FINE)) { return super.logAndRebufferResponse(configKey, logLevel, response, elapsedTime); } @@ -110,16 +109,14 @@ public String format(LogRecord record) { } public static class NoOpLogger extends Logger { - @Override void logRequest(String configKey, Level logLevel, Request request) { + @Override protected void logRequest(String configKey, Level logLevel, Request request) { } - @Override - Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime) throws IOException { + @Override protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime) throws IOException { return response; } - @Override - protected void log(String configKey, String format, Object... args) { + @Override protected void log(String configKey, String format, Object... args) { } } @@ -133,7 +130,7 @@ protected void log(String configKey, String format, Object... args) { */ protected abstract void log(String configKey, String format, Object... args); - void logRequest(String configKey, Level logLevel, Request request) { + protected 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()) { @@ -160,7 +157,7 @@ void logRetry(String configKey, Level logLevel) { log(configKey, "---> RETRYING"); } - Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime) throws IOException { + protected 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()) { @@ -200,7 +197,7 @@ IOException logIOException(String configKey, Level logLevel, IOException ioe, lo return ioe; } - static String methodTag(String configKey) { + protected static String methodTag(String configKey) { return new StringBuilder().append('[').append(configKey.substring(0, configKey.indexOf('('))).append("] ").toString(); } } diff --git a/settings.gradle b/settings.gradle index 8dac555cc9..89c278128f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,5 @@ rootProject.name='feign' -include 'core', 'sax', 'gson', 'jackson', 'jaxrs', 'ribbon', 'example-github', 'example-wikipedia' +include 'core', 'sax', 'gson', 'jackson', 'jaxrs', 'ribbon', 'slf4j', 'example-github', 'example-wikipedia' rootProject.children.each { childProject -> childProject.name = 'feign-' + childProject.name diff --git a/slf4j/README.md b/slf4j/README.md new file mode 100644 index 0000000000..e2c21fd0a2 --- /dev/null +++ b/slf4j/README.md @@ -0,0 +1,12 @@ +SLF4J +=================== + +This module allows directing Feign's logging to [SLF4J](http://www.slf4j.org/), allowing you to easily use a logging backend of your choice (Logback, Log4J, etc.) + +To use SLF4J with Feign, add both the SLF4J module and an SLF4J binding of your choice to your classpath. Then, configure Feign to use the Slf4jLogger: + +```java +GitHub github = Feign.builder() + .logger(new Slf4jLogger()) + .target(GitHub.class, "https://api.github.com"); +``` diff --git a/slf4j/src/main/java/feign/slf4j/Slf4jLogger.java b/slf4j/src/main/java/feign/slf4j/Slf4jLogger.java new file mode 100644 index 0000000000..724d7c60ba --- /dev/null +++ b/slf4j/src/main/java/feign/slf4j/Slf4jLogger.java @@ -0,0 +1,66 @@ +/* + * 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.slf4j; + +import feign.Request; +import feign.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +/** + * Logs to SLF4J at the debug level, if the underlying logger has debug logging enabled. The underlying logger can + * be specified at construction-time, defaulting to the logger for {@link feign.Logger}. + */ +public class Slf4jLogger extends feign.Logger { + private final Logger logger; + + public Slf4jLogger() { + this(feign.Logger.class); + } + + public Slf4jLogger(Class clazz) { + this(LoggerFactory.getLogger(clazz)); + } + + public Slf4jLogger(String name) { + this(LoggerFactory.getLogger(name)); + } + + Slf4jLogger(Logger logger) { + this.logger = logger; + } + + @Override protected void logRequest(String configKey, Level logLevel, Request request) { + if (logger.isDebugEnabled()) { + super.logRequest(configKey, logLevel, request); + } + } + + @Override protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime) throws IOException { + if (logger.isDebugEnabled()) { + return super.logAndRebufferResponse(configKey, logLevel, response, elapsedTime); + } + return response; + } + + @Override protected void log(String configKey, String format, Object... args) { + // Not using SLF4J's support for parameterized messages (even though it would be more efficient) because it would + // require the incoming message formats to be SLF4J-specific. + logger.debug(String.format(methodTag(configKey) + format, args)); + } +} diff --git a/slf4j/src/test/java/feign/slf4j/ReflectionUtil.java b/slf4j/src/test/java/feign/slf4j/ReflectionUtil.java new file mode 100644 index 0000000000..2fa083bc68 --- /dev/null +++ b/slf4j/src/test/java/feign/slf4j/ReflectionUtil.java @@ -0,0 +1,37 @@ +/* + * 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.slf4j; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +/** + * Lightweight approach to using reflection to bypass access restrictions for testing. If this class grows, it may be + * better to use a testing library instead, such as Powermock. + */ +class ReflectionUtil { + static void setStaticField(Class declaringClass, String fieldName, Object fieldValue) throws Exception { + Field field = declaringClass.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(null, fieldValue); + } + + static void invokeVoidNoArgMethod(Class declaringClass, String methodName, Object instance) throws Exception { + Method method = declaringClass.getDeclaredMethod(methodName); + method.setAccessible(true); + method.invoke(instance); + } +} diff --git a/slf4j/src/test/java/feign/slf4j/SimpleLoggerUtil.java b/slf4j/src/test/java/feign/slf4j/SimpleLoggerUtil.java new file mode 100644 index 0000000000..e676e1470e --- /dev/null +++ b/slf4j/src/test/java/feign/slf4j/SimpleLoggerUtil.java @@ -0,0 +1,47 @@ +/* + * 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.slf4j; + +import org.slf4j.LoggerFactory; +import org.slf4j.impl.SimpleLogger; +import org.slf4j.impl.SimpleLoggerFactory; + +import java.io.File; + +/** + * A testing utility to allow control over {@link SimpleLogger}. In some cases, reflection is used to bypass access + * restrictions. + */ +class SimpleLoggerUtil { + static void initialize(File file, String logLevel) throws Exception { + System.setProperty(SimpleLogger.SHOW_THREAD_NAME_KEY, "false"); + System.setProperty(SimpleLogger.LOG_FILE_KEY, file.getAbsolutePath()); + System.setProperty(SimpleLogger.DEFAULT_LOG_LEVEL_KEY, logLevel); + resetSlf4j(); + } + + static void resetToDefaults() throws Exception { + System.clearProperty(SimpleLogger.SHOW_THREAD_NAME_KEY); + System.clearProperty(SimpleLogger.LOG_FILE_KEY); + System.clearProperty(SimpleLogger.DEFAULT_LOG_LEVEL_KEY); + resetSlf4j(); + } + + private static void resetSlf4j() throws Exception { + ReflectionUtil.setStaticField(SimpleLogger.class, "INITIALIZED", false); + ReflectionUtil.invokeVoidNoArgMethod(SimpleLoggerFactory.class, "reset", LoggerFactory.getILoggerFactory()); + } +} diff --git a/slf4j/src/test/java/feign/slf4j/Slf4jLoggerTest.java b/slf4j/src/test/java/feign/slf4j/Slf4jLoggerTest.java new file mode 100644 index 0000000000..8b4ec16f2c --- /dev/null +++ b/slf4j/src/test/java/feign/slf4j/Slf4jLoggerTest.java @@ -0,0 +1,109 @@ +/* + * 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.slf4j; + +import feign.Feign; +import feign.Logger; +import feign.Request; +import feign.RequestTemplate; +import feign.Response; +import feign.Util; +import org.slf4j.LoggerFactory; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.Test; + +import java.io.File; +import java.io.FileReader; +import java.util.Collection; +import java.util.Collections; + +import static org.testng.Assert.assertEquals; + +public class Slf4jLoggerTest { + private static final String CONFIG_KEY = "someMethod()"; + private static final Request REQUEST = + new RequestTemplate().method("GET").append("http://api.example.com").request(); + private static final Response RESPONSE = + Response.create(200, "OK", Collections.>emptyMap(), new byte[0]); + + private File logFile; + private Slf4jLogger logger; + + @AfterMethod + void tearDown() throws Exception { + SimpleLoggerUtil.resetToDefaults(); + logFile.delete(); + } + + @Test public void useFeignLoggerByDefault() throws Exception { + initializeSimpleLogger("debug"); + logger = new Slf4jLogger(); + logger.log(CONFIG_KEY, "This is my message"); + assertLoggedMessages("DEBUG feign.Logger - [someMethod] This is my message\n"); + } + + @Test public void useLoggerByNameIfRequested() throws Exception { + initializeSimpleLogger("debug"); + logger = new Slf4jLogger("named.logger"); + logger.log(CONFIG_KEY, "This is my message"); + assertLoggedMessages("DEBUG named.logger - [someMethod] This is my message\n"); + } + + @Test public void useLoggerByClassIfRequested() throws Exception { + initializeSimpleLogger("debug"); + logger = new Slf4jLogger(Feign.class); + logger.log(CONFIG_KEY, "This is my message"); + assertLoggedMessages("DEBUG feign.Feign - [someMethod] This is my message\n"); + } + + @Test public void useSpecifiedLoggerIfRequested() throws Exception { + initializeSimpleLogger("debug"); + logger = new Slf4jLogger(LoggerFactory.getLogger("specified.logger")); + logger.log(CONFIG_KEY, "This is my message"); + assertLoggedMessages("DEBUG specified.logger - [someMethod] This is my message\n"); + } + + @Test public void logOnlyIfDebugEnabled() throws Exception { + initializeSimpleLogger("info"); + logger = new Slf4jLogger(); + logger.log(CONFIG_KEY, "A message with %d formatting %s.", 2, "tokens"); + logger.logRequest(CONFIG_KEY, Logger.Level.BASIC, REQUEST); + logger.logAndRebufferResponse(CONFIG_KEY, Logger.Level.BASIC, RESPONSE, 273); + assertLoggedMessages(""); + } + + @Test public void logRequestsAndResponses() throws Exception { + initializeSimpleLogger("debug"); + logger = new Slf4jLogger(); + logger.log(CONFIG_KEY, "A message with %d formatting %s.", 2, "tokens"); + logger.logRequest(CONFIG_KEY, Logger.Level.BASIC, REQUEST); + logger.logAndRebufferResponse(CONFIG_KEY, Logger.Level.BASIC, RESPONSE, 273); + assertLoggedMessages( + "DEBUG feign.Logger - [someMethod] A message with 2 formatting tokens.\n" + + "DEBUG feign.Logger - [someMethod] ---> GET http://api.example.com HTTP/1.1\n" + + "DEBUG feign.Logger - [someMethod] <--- HTTP/1.1 200 OK (273ms)\n" + ); + } + + private void initializeSimpleLogger(String logLevel) throws Exception { + logFile = File.createTempFile(getClass().getName(), ".log"); + SimpleLoggerUtil.initialize(logFile, logLevel); + } + + private void assertLoggedMessages(String expectedMessages) throws Exception { + assertEquals(Util.toString(new FileReader(logFile)), expectedMessages); + } +} From 9a7e84e158bd671e5debff8680aaef9982aef8d6 Mon Sep 17 00:00:00 2001 From: "julian.duniec" Date: Thu, 30 Jan 2014 14:38:24 +0100 Subject: [PATCH 130/179] Fix for bug in Ribbon-Module, where query strings are not properly encoded --- core/src/main/java/feign/RequestTemplate.java | 29 +++++++++++++- .../java/feign/ribbon/RibbonClientTest.java | 40 +++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/feign/RequestTemplate.java b/core/src/main/java/feign/RequestTemplate.java index 6a3916593a..ad51742660 100644 --- a/core/src/main/java/feign/RequestTemplate.java +++ b/core/src/main/java/feign/RequestTemplate.java @@ -463,13 +463,38 @@ private StringBuilder pullAnyQueriesOutOfUrl(StringBuilder url) { firstQueries.putAll(queries); queries.clear(); } - queries.putAll(firstQueries); + //Since we decode all queries, we want to use the + //query()-method to re-add them to ensure that all + //logic (such as url-encoding) are executed, giving + //a valid queryLine() + for(String key : firstQueries.keySet()) { + Collection values = firstQueries.get(key); + if(allValuesAreNull(values)) { + //Queryies where all values are null will + //be ignored by the query(key, value)-method + //So we manually avoid this case here, to ensure that + //we still fulfill the contract (ex. parameters without values) + queries.put(urlEncode(key), values); + } + else { + query(key, values); + } + + } return new StringBuilder(url.substring(0, queryIndex)); } return url; } - private static Map> parseAndDecodeQueries(String queryLine) { + private boolean allValuesAreNull(Collection values) { + if(values.isEmpty()) return true; + for(String val : values) { + if(val != null) return false; + } + return true; + } + + private static Map> parseAndDecodeQueries(String queryLine) { Map> map = new LinkedHashMap>(); if (emptyToNull(queryLine) == null) return map; diff --git a/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java b/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java index d691b94cc8..fb97b8debd 100644 --- a/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java +++ b/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java @@ -32,10 +32,13 @@ import static feign.Util.UTF_8; import static org.testng.Assert.assertEquals; +import javax.inject.Named; + @Test public class RibbonClientTest { interface TestInterface { @RequestLine("POST /") void post(); + @RequestLine("GET /?a={a}") void getWithQueryParameters(@Named("a") String a); @dagger.Module(injects = Feign.class, overrides = true, addsTo = Feign.Defaults.class) static class Module { @@ -108,6 +111,43 @@ public void ioExceptionRetry() throws IOException, InterruptedException { } } + /* + This test-case replicates a bug that occurs when using RibbonRequest with a query string. + + The querystrings would not be URL-encoded, leading to invalid HTTP-requests if the query string contained + invalid characters (ex. space). + */ + @Test public void urlEncodeQueryStringParameters () throws IOException, InterruptedException { + String client = "RibbonClientTest-urlEncodeQueryStringParameters"; + String serverListKey = client + ".ribbon.listOfServers"; + + String queryStringValue = "some string with space"; + String expectedQueryStringValue = "some+string+with+space"; + String expectedRequestLine = String.format("GET /?a=%s HTTP/1.1", expectedQueryStringValue); + + MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8))); + server.play(); + + getConfigInstance().setProperty(serverListKey, hostAndPort(server.getUrl(""))); + + try { + + TestInterface api = Feign.create(TestInterface.class, "http://" + client, new TestInterface.Module(), new RibbonModule()); + + api.getWithQueryParameters(queryStringValue); + + final String recordedRequestLine = server.takeRequest().getRequestLine(); + + assertEquals(recordedRequestLine, expectedRequestLine); + } finally { + server.shutdown(); + getConfigInstance().clearProperty(serverListKey); + } + } + + + static String hostAndPort(URL url) { // our build slaves have underscores in their hostnames which aren't permitted by ribbon return "localhost:" + url.getPort(); From 00db9c4e0e4dc9d749fa2fb9ee8e054454e64951 Mon Sep 17 00:00:00 2001 From: Wolfgang Nagele Date: Mon, 10 Feb 2014 21:58:57 +1100 Subject: [PATCH 131/179] Fix for bug #85 --- CHANGES.md | 3 +++ core/src/main/java/feign/Contract.java | 21 ++++++++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 8c67a296b6..8279a421cb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,6 @@ +### Version 6.1.1 +* Fix for #85 + ### Version 6.1.0 * Add [SLF4J](http://www.slf4j.org/) integration diff --git a/core/src/main/java/feign/Contract.java b/core/src/main/java/feign/Contract.java index 813247401c..d9ac3bd110 100644 --- a/core/src/main/java/feign/Contract.java +++ b/core/src/main/java/feign/Contract.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Map; import static feign.Util.checkState; import static feign.Util.emptyToNull; @@ -165,14 +166,28 @@ protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[ checkState(emptyToNull(name) != null, "Named annotation was empty on param %s.", paramIndex); nameParam(data, name, paramIndex); isHttpAnnotation = true; - if (data.template().url().indexOf('{' + name + '}') == -1 && // - !(data.template().queries().containsKey(name) - || data.template().headers().containsKey(name))) { + String varName = '{' + name + '}'; + if (data.template().url().indexOf(varName) == -1 && + !searchMapValues(data.template().queries(), varName) && + !searchMapValues(data.template().headers(), varName)) { data.formParams().add(name); } } } return isHttpAnnotation; } + + private boolean searchMapValues(Map> map, V search) { + Collection> values = map.values(); + if (values == null) + return false; + + for (Collection entry : values) { + if (entry.contains(search)) + return true; + } + + return false; + } } } From ac7f0ecd4a4051018b2d44c5e76f953c136dfbe2 Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Sun, 25 May 2014 09:03:26 -0700 Subject: [PATCH 132/179] Make build work with java 8. --- build.gradle | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 319ed7f9bc..cb90e5e40b 100644 --- a/build.gradle +++ b/build.gradle @@ -10,6 +10,11 @@ buildscript { } allprojects { + if (JavaVersion.current().isJava8Compatible()) { + tasks.withType(Javadoc) { + options.addStringOption('Xdoclint:none', '-quiet') // Doclint is onerous in Java 8. + } + } repositories { mavenLocal() mavenCentral() @@ -19,7 +24,9 @@ allprojects { apply from: file('gradle/convention.gradle') apply from: file('gradle/maven.gradle') -apply from: file('gradle/check.gradle') +if (!JavaVersion.current().isJava8Compatible()) { + apply from: file('gradle/check.gradle') // FindBugs is incompatible with Java 8. +} apply from: file('gradle/license.gradle') apply from: file('gradle/release.gradle') apply plugin: 'idea' From 95a0f6bf141d70b8bd0d093f47044810b26b1435 Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Sun, 25 May 2014 11:54:29 -0700 Subject: [PATCH 133/179] Fix #105: expose hook for reflective dispatch. --- CHANGES.md | 3 + core/src/main/java/feign/Feign.java | 17 +- .../java/feign/InvocationHandlerFactory.java | 37 ++++ core/src/main/java/feign/MethodHandler.java | 186 ------------------ core/src/main/java/feign/ReflectiveFeign.java | 41 ++-- core/src/main/java/feign/RequestTemplate.java | 4 + .../java/feign/SynchronousMethodHandler.java | 176 +++++++++++++++++ .../src/test/java/feign/FeignBuilderTest.java | 33 ++++ 8 files changed, 288 insertions(+), 209 deletions(-) create mode 100644 core/src/main/java/feign/InvocationHandlerFactory.java delete mode 100644 core/src/main/java/feign/MethodHandler.java create mode 100644 core/src/main/java/feign/SynchronousMethodHandler.java diff --git a/CHANGES.md b/CHANGES.md index 8279a421cb..773413dbbf 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,6 @@ +### Version 7.0 +* Expose reflective dispatch hook: InvocationHandlerFactory + ### Version 6.1.1 * Fix for #85 diff --git a/core/src/main/java/feign/Feign.java b/core/src/main/java/feign/Feign.java index c08bf16312..cc5bd597e2 100644 --- a/core/src/main/java/feign/Feign.java +++ b/core/src/main/java/feign/Feign.java @@ -120,9 +120,13 @@ public static class Defaults { return HttpsURLConnection.getDefaultHostnameVerifier(); } - @Provides feign.Client httpClient(feign.Client.Default client) { + @Provides Client httpClient(Client.Default client) { return client; } + + @Provides InvocationHandlerFactory invocationHandlerFactory() { + return new InvocationHandlerFactory.Default(); + } } /** @@ -177,6 +181,7 @@ public static class Builder { Decoder decoder = new Decoder.Default(); @Inject ErrorDecoder errorDecoder; @Inject Options options; + @Inject InvocationHandlerFactory invocationHandlerFactory; Builder() { ObjectGraph.create(new Defaults()).inject(this); @@ -246,6 +251,12 @@ public Builder requestInterceptors(Iterable requestIntercept return this; } + /** Allows you to override how reflective dispatch works inside of Feign. */ + public Builder invocationHandlerFactory(InvocationHandlerFactory invocationHandlerFactory) { + this.invocationHandlerFactory = invocationHandlerFactory; + return this; + } + public T target(Class apiType, String url) { return target(new HardCodedTarget(apiType, url)); } @@ -293,5 +304,9 @@ public T target(Target target) { @Provides(type = Provides.Type.SET_VALUES) Set requestInterceptors() { return requestInterceptors; } + + @Provides InvocationHandlerFactory invocationHandlerFactory() { + return invocationHandlerFactory; + } } } diff --git a/core/src/main/java/feign/InvocationHandlerFactory.java b/core/src/main/java/feign/InvocationHandlerFactory.java new file mode 100644 index 0000000000..cf8080492e --- /dev/null +++ b/core/src/main/java/feign/InvocationHandlerFactory.java @@ -0,0 +1,37 @@ +/* + * Copyright 2014 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.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.util.Map; + +/** Controls reflective method dispatch. */ +public interface InvocationHandlerFactory { + /** Like {@link InvocationHandler#invoke(Object, java.lang.reflect.Method, Object[])}, except for a single method. */ + interface MethodHandler { + Object invoke(Object[] argv) throws Throwable; + } + + InvocationHandler create(Target target, Map dispatch); + + static final class Default implements InvocationHandlerFactory { + @Override + public InvocationHandler create(Target target, Map dispatch) { + return new ReflectiveFeign.FeignInvocationHandler(target, dispatch); + } + } +} diff --git a/core/src/main/java/feign/MethodHandler.java b/core/src/main/java/feign/MethodHandler.java deleted file mode 100644 index 141e644252..0000000000 --- a/core/src/main/java/feign/MethodHandler.java +++ /dev/null @@ -1,186 +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 feign.Request.Options; -import feign.codec.DecodeException; -import feign.codec.Decoder; -import feign.codec.ErrorDecoder; - -import javax.inject.Inject; -import javax.inject.Provider; -import java.io.IOException; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -import static feign.FeignException.errorExecuting; -import static feign.FeignException.errorReading; -import static feign.Util.checkNotNull; -import static feign.Util.ensureClosed; - -interface MethodHandler { - Object invoke(Object[] argv) throws Throwable; - - static class Factory { - - private final Client client; - private final Provider retryer; - private final Set requestInterceptors; - private final Logger logger; - private final Provider logLevel; - - @Inject Factory(Client client, Provider retryer, Set requestInterceptors, - Logger logger, Provider logLevel) { - this.client = checkNotNull(client, "client"); - this.retryer = checkNotNull(retryer, "retryer"); - this.requestInterceptors = checkNotNull(requestInterceptors, "requestInterceptors"); - this.logger = checkNotNull(logger, "logger"); - this.logLevel = checkNotNull(logLevel, "logLevel"); - } - - public MethodHandler create(Target target, MethodMetadata md, BuildTemplateFromArgs buildTemplateFromArgs, - Options options, Decoder decoder, ErrorDecoder errorDecoder) { - return new SynchronousMethodHandler(target, client, retryer, requestInterceptors, logger, logLevel, md, - buildTemplateFromArgs, options, decoder, errorDecoder); - } - } - - /** - * Those using guava will implement as {@code Function}. - */ - interface BuildTemplateFromArgs { - public RequestTemplate apply(Object[] argv); - } - - static final class SynchronousMethodHandler implements MethodHandler { - - private final MethodMetadata metadata; - private final Target target; - private final Client client; - private final Provider retryer; - private final Set requestInterceptors; - private final Logger logger; - private final Provider logLevel; - private final BuildTemplateFromArgs buildTemplateFromArgs; - private final Options options; - private final Decoder decoder; - private final ErrorDecoder errorDecoder; - - private SynchronousMethodHandler(Target target, Client client, Provider retryer, - Set requestInterceptors, Logger logger, - Provider logLevel, MethodMetadata metadata, - BuildTemplateFromArgs buildTemplateFromArgs, Options options, - Decoder decoder, 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); - this.buildTemplateFromArgs = checkNotNull(buildTemplateFromArgs, "metadata for %s", target); - this.options = checkNotNull(options, "options for %s", target); - this.errorDecoder = checkNotNull(errorDecoder, "errorDecoder for %s", target); - this.decoder = checkNotNull(decoder, "decoder for %s", target); - } - - @Override public Object invoke(Object[] argv) throws Throwable { - RequestTemplate template = buildTemplateFromArgs.apply(argv); - Retryer retryer = this.retryer.get(); - while (true) { - try { - return executeAndDecode(template); - } catch (RetryableException e) { - retryer.continueOrPropagate(e); - if (logLevel.get() != Logger.Level.NONE) { - logger.logRetry(metadata.configKey(), logLevel.get()); - } - continue; - } - } - } - - Object executeAndDecode(RequestTemplate template) throws Throwable { - Request request = targetRequest(template); - - if (logLevel.get() != Logger.Level.NONE) { - logger.logRequest(metadata.configKey(), logLevel.get(), request); - } - - Response response; - long start = System.nanoTime(); - 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.get() != Logger.Level.NONE) { - response = logger.logAndRebufferResponse(metadata.configKey(), logLevel.get(), response, elapsedTime); - } - if (response.status() >= 200 && response.status() < 300) { - if (Response.class == metadata.returnType()) { - if (response.body() == null) { - return response; - } - // Ensure the response body is disconnected - byte[] bodyData = Util.toByteArray(response.body().asInputStream()); - return Response.create(response.status(), response.reason(), response.headers(), bodyData); - } else if (void.class == metadata.returnType()) { - return null; - } else { - return decode(response); - } - } else { - 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()); - } - } - - long elapsedTime(long start) { - return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); - } - - Request targetRequest(RequestTemplate template) { - for (RequestInterceptor interceptor : requestInterceptors) { - interceptor.apply(template); - } - return target.apply(new RequestTemplate(template)); - } - - Object decode(Response response) throws Throwable { - try { - return decoder.decode(response, metadata.returnType()); - } catch (FeignException e) { - throw e; - } catch (RuntimeException e) { - throw new DecodeException(e.getMessage(), e); - } - } - } -} diff --git a/core/src/main/java/feign/ReflectiveFeign.java b/core/src/main/java/feign/ReflectiveFeign.java index b1d60690f4..5d8fe06841 100644 --- a/core/src/main/java/feign/ReflectiveFeign.java +++ b/core/src/main/java/feign/ReflectiveFeign.java @@ -16,6 +16,7 @@ package feign; import dagger.Provides; +import feign.InvocationHandlerFactory.MethodHandler; import feign.Request.Options; import feign.codec.Decoder; import feign.codec.EncodeException; @@ -41,9 +42,11 @@ public class ReflectiveFeign extends Feign { private final ParseHandlersByName targetToHandlersByName; + private final InvocationHandlerFactory factory; - @Inject ReflectiveFeign(ParseHandlersByName targetToHandlersByName) { + @Inject ReflectiveFeign(ParseHandlersByName targetToHandlersByName, InvocationHandlerFactory factory) { this.targetToHandlersByName = targetToHandlersByName; + this.factory = factory; } /** @@ -58,18 +61,18 @@ public class ReflectiveFeign extends Feign { continue; methodToHandler.put(method, nameToHandler.get(Feign.configKey(method))); } - FeignInvocationHandler handler = new FeignInvocationHandler(target, methodToHandler); + InvocationHandler handler = factory.create(target, methodToHandler); return (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class[]{target.type()}, handler); } static class FeignInvocationHandler implements InvocationHandler { private final Target target; - private final Map methodToHandler; + private final Map dispatch; - FeignInvocationHandler(Target target, Map methodToHandler) { + FeignInvocationHandler(Target target, Map dispatch) { this.target = checkNotNull(target, "target"); - this.methodToHandler = checkNotNull(methodToHandler, "methodToHandler for %s", target); + this.dispatch = checkNotNull(dispatch, "dispatch for %s", target); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { @@ -84,25 +87,19 @@ static class FeignInvocationHandler implements InvocationHandler { if ("hashCode".equals(method.getName())) { return hashCode(); } - return methodToHandler.get(method).invoke(args); + return dispatch.get(method).invoke(args); } @Override public int hashCode() { return target.hashCode(); } - @Override public boolean equals(Object obj) { - if (obj == null) { - return false; + @Override public boolean equals(Object other) { + if (other instanceof FeignInvocationHandler) { + FeignInvocationHandler that = (FeignInvocationHandler) other; + return this.target.equals(that.target); } - if (this == obj) { - return true; - } - if (FeignInvocationHandler.class != obj.getClass()) { - return false; - } - FeignInvocationHandler that = FeignInvocationHandler.class.cast(obj); - return this.target.equals(that.target); + return false; } @Override public String toString() { @@ -110,7 +107,7 @@ static class FeignInvocationHandler implements InvocationHandler { } } - @dagger.Module(complete = false, injects = {Feign.class, MethodHandler.Factory.class}, library = true) + @dagger.Module(complete = false, injects = {Feign.class, SynchronousMethodHandler.Factory.class}, library = true) public static class Module { @Provides(type = Provides.Type.SET_VALUES) Set noRequestInterceptors() { return Collections.emptySet(); @@ -127,11 +124,11 @@ static final class ParseHandlersByName { private final Encoder encoder; private final Decoder decoder; private final ErrorDecoder errorDecoder; - private final MethodHandler.Factory factory; + private final SynchronousMethodHandler.Factory factory; @SuppressWarnings("unchecked") @Inject ParseHandlersByName(Contract contract, Options options, Encoder encoder, Decoder decoder, - ErrorDecoder errorDecoder, MethodHandler.Factory factory) { + ErrorDecoder errorDecoder, SynchronousMethodHandler.Factory factory) { this.contract = contract; this.options = options; this.factory = factory; @@ -158,14 +155,14 @@ public Map apply(Target key) { } } - private static class BuildTemplateByResolvingArgs implements MethodHandler.BuildTemplateFromArgs { + private static class BuildTemplateByResolvingArgs implements RequestTemplate.Factory { protected final MethodMetadata metadata; private BuildTemplateByResolvingArgs(MethodMetadata metadata) { this.metadata = metadata; } - public RequestTemplate apply(Object[] argv) { + @Override public RequestTemplate create(Object[] argv) { RequestTemplate mutable = new RequestTemplate(metadata.template()); if (metadata.urlIndex() != null) { int urlIndex = metadata.urlIndex(); diff --git a/core/src/main/java/feign/RequestTemplate.java b/core/src/main/java/feign/RequestTemplate.java index ad51742660..42c6b9046e 100644 --- a/core/src/main/java/feign/RequestTemplate.java +++ b/core/src/main/java/feign/RequestTemplate.java @@ -49,6 +49,10 @@ */ public final class RequestTemplate implements Serializable { + interface Factory { + /** create a request template using args passed to a method invocation. */ + RequestTemplate create(Object[] argv); + } private String method; /* final to encourage mutable use vs replacing the object. */ diff --git a/core/src/main/java/feign/SynchronousMethodHandler.java b/core/src/main/java/feign/SynchronousMethodHandler.java new file mode 100644 index 0000000000..83c102da45 --- /dev/null +++ b/core/src/main/java/feign/SynchronousMethodHandler.java @@ -0,0 +1,176 @@ +/* + * Copyright 2014 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 feign.InvocationHandlerFactory.MethodHandler; +import feign.Request.Options; +import feign.codec.DecodeException; +import feign.codec.Decoder; +import feign.codec.ErrorDecoder; + +import javax.inject.Inject; +import javax.inject.Provider; +import java.io.IOException; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import static feign.FeignException.errorExecuting; +import static feign.FeignException.errorReading; +import static feign.Util.checkNotNull; +import static feign.Util.ensureClosed; + +final class SynchronousMethodHandler implements MethodHandler { + + static class Factory { + + private final Client client; + private final Provider retryer; + private final Set requestInterceptors; + private final Logger logger; + private final Provider logLevel; + + @Inject Factory(Client client, Provider retryer, Set requestInterceptors, + Logger logger, Provider logLevel) { + this.client = checkNotNull(client, "client"); + this.retryer = checkNotNull(retryer, "retryer"); + this.requestInterceptors = checkNotNull(requestInterceptors, "requestInterceptors"); + this.logger = checkNotNull(logger, "logger"); + this.logLevel = checkNotNull(logLevel, "logLevel"); + } + + public MethodHandler create(Target target, MethodMetadata md, RequestTemplate.Factory buildTemplateFromArgs, + Options options, Decoder decoder, ErrorDecoder errorDecoder) { + return new SynchronousMethodHandler(target, client, retryer, requestInterceptors, logger, logLevel, md, + buildTemplateFromArgs, options, decoder, errorDecoder); + } + } + + private final MethodMetadata metadata; + private final Target target; + private final Client client; + private final Provider retryer; + private final Set requestInterceptors; + private final Logger logger; + private final Provider logLevel; + private final RequestTemplate.Factory buildTemplateFromArgs; + private final Options options; + private final Decoder decoder; + private final ErrorDecoder errorDecoder; + + private SynchronousMethodHandler(Target target, Client client, Provider retryer, + Set requestInterceptors, Logger logger, + Provider logLevel, MethodMetadata metadata, + RequestTemplate.Factory buildTemplateFromArgs, Options options, + Decoder decoder, 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); + this.buildTemplateFromArgs = checkNotNull(buildTemplateFromArgs, "metadata for %s", target); + this.options = checkNotNull(options, "options for %s", target); + this.errorDecoder = checkNotNull(errorDecoder, "errorDecoder for %s", target); + this.decoder = checkNotNull(decoder, "decoder for %s", target); + } + + @Override public Object invoke(Object[] argv) throws Throwable { + RequestTemplate template = buildTemplateFromArgs.create(argv); + Retryer retryer = this.retryer.get(); + while (true) { + try { + return executeAndDecode(template); + } catch (RetryableException e) { + retryer.continueOrPropagate(e); + if (logLevel.get() != Logger.Level.NONE) { + logger.logRetry(metadata.configKey(), logLevel.get()); + } + continue; + } + } + } + + Object executeAndDecode(RequestTemplate template) throws Throwable { + Request request = targetRequest(template); + + if (logLevel.get() != Logger.Level.NONE) { + logger.logRequest(metadata.configKey(), logLevel.get(), request); + } + + Response response; + long start = System.nanoTime(); + 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.get() != Logger.Level.NONE) { + response = logger.logAndRebufferResponse(metadata.configKey(), logLevel.get(), response, elapsedTime); + } + if (response.status() >= 200 && response.status() < 300) { + if (Response.class == metadata.returnType()) { + if (response.body() == null) { + return response; + } + // Ensure the response body is disconnected + byte[] bodyData = Util.toByteArray(response.body().asInputStream()); + return Response.create(response.status(), response.reason(), response.headers(), bodyData); + } else if (void.class == metadata.returnType()) { + return null; + } else { + return decode(response); + } + } else { + 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()); + } + } + + long elapsedTime(long start) { + return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); + } + + Request targetRequest(RequestTemplate template) { + for (RequestInterceptor interceptor : requestInterceptors) { + interceptor.apply(template); + } + return target.apply(new RequestTemplate(template)); + } + + Object decode(Response response) throws Throwable { + try { + return decoder.decode(response, metadata.returnType()); + } catch (FeignException e) { + throw e; + } catch (RuntimeException e) { + throw new DecodeException(e.getMessage(), e); + } + } +} diff --git a/core/src/test/java/feign/FeignBuilderTest.java b/core/src/test/java/feign/FeignBuilderTest.java index 097e80aca3..c9a3ef8f20 100644 --- a/core/src/test/java/feign/FeignBuilderTest.java +++ b/core/src/test/java/feign/FeignBuilderTest.java @@ -23,9 +23,13 @@ import feign.codec.Encoder; import org.testng.annotations.Test; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.Arrays; import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; import static org.testng.Assert.assertEquals; @@ -123,4 +127,33 @@ public void apply(RequestTemplate template) { assertEquals(request.getHeader("Content-Type"), "text/plain"); } } + + @Test public void testProvideInvocationHandlerFactory() throws Exception { + MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse().setBody("response data")); + server.play(); + + String url = "http://localhost:" + server.getPort(); + + final AtomicInteger callCount = new AtomicInteger(); + InvocationHandlerFactory factory = new InvocationHandlerFactory() { + private final InvocationHandlerFactory delegate = new Default(); + @Override public InvocationHandler create(Target target, Map dispatch) { + callCount.incrementAndGet(); + return delegate.create(target, dispatch); + } + }; + + try { + TestInterface api = Feign.builder().invocationHandlerFactory(factory).target(TestInterface.class, url); + Response response = api.codecPost("request data"); + assertEquals(Util.toString(response.body().asReader()), "response data"); + assertEquals(callCount.get(), 1); + } finally { + server.shutdown(); + assertEquals(server.getRequestCount(), 1); + RecordedRequest request = server.takeRequest(); + assertEquals(request.getUtf8Body(), "request data"); + } + } } From 8756f99aa054884d12958c514c83dd70a69be2cc Mon Sep 17 00:00:00 2001 From: Allen Wang Date: Thu, 29 May 2014 16:07:29 -0700 Subject: [PATCH 134/179] Change minor versions of dependencies to help snapshot build. --- build.gradle | 2 +- example-github/build.gradle | 4 ++-- example-wikipedia/build.gradle | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index cb90e5e40b..9e11a60276 100644 --- a/build.gradle +++ b/build.gradle @@ -120,7 +120,7 @@ project(':feign-ribbon') { dependencies { compile project(':feign-core') - compile 'com.netflix.ribbon:ribbon-core:0.2.3' + compile 'com.netflix.ribbon:ribbon-core:0.2.4' testCompile 'org.testng:testng:6.8.5' testCompile 'com.google.mockwebserver:mockwebserver:20130706' } diff --git a/example-github/build.gradle b/example-github/build.gradle index 24049dc0c6..8b92037caf 100644 --- a/example-github/build.gradle +++ b/example-github/build.gradle @@ -1,8 +1,8 @@ apply plugin: 'java' dependencies { - compile 'com.netflix.feign:feign-core:5.0.0' - compile 'com.netflix.feign:feign-gson:5.0.0' + compile 'com.netflix.feign:feign-core:5.3.0' + compile 'com.netflix.feign:feign-gson:5.3.0' provided 'com.squareup.dagger:dagger-compiler:1.1.0' } diff --git a/example-wikipedia/build.gradle b/example-wikipedia/build.gradle index 73c6b99624..9a85fd6165 100644 --- a/example-wikipedia/build.gradle +++ b/example-wikipedia/build.gradle @@ -1,8 +1,8 @@ apply plugin: 'java' dependencies { - compile 'com.netflix.feign:feign-core:5.0.0' - compile 'com.netflix.feign:feign-gson:5.0.0' + compile 'com.netflix.feign:feign-core:5.3.0' + compile 'com.netflix.feign:feign-gson:5.3.0' provided 'com.squareup.dagger:dagger-compiler:1.1.0' } From ee54f394c4262fd0b56e149cd21839092c770a95 Mon Sep 17 00:00:00 2001 From: sheller Date: Wed, 4 Jun 2014 14:23:40 -0400 Subject: [PATCH 135/179] Handle JAXRS Path annotation processes without slashes --- .../main/java/feign/jaxrs/JAXRSModule.java | 9 +++++- .../java/feign/jaxrs/JAXRSContractTest.java | 30 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java b/jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java index dc84c8e2d0..1560058f3c 100644 --- a/jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java +++ b/jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java @@ -57,6 +57,9 @@ public MethodMetadata parseAndValidatateMetadata(Method method) { if (path != null) { String pathValue = emptyToNull(path.value()); checkState(pathValue != null, "Path.value() was empty on type %s", method.getDeclaringClass().getName()); + if (!pathValue.startsWith("/")) { + pathValue = "/" + pathValue; + } md.template().insert(0, pathValue); } return md; @@ -74,7 +77,11 @@ protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodA } 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()); + String methodAnnotationValue = Path.class.cast(methodAnnotation).value(); + if (!methodAnnotationValue.startsWith("/") && !data.template().toString().endsWith("/")) { + methodAnnotationValue = "/" + methodAnnotationValue; + } + data.template().append(methodAnnotationValue); } else if (annotationType == Produces.class) { String[] serverProduces = ((Produces) methodAnnotation).value(); String clientAccepts = serverProduces.length == 0 ? null: emptyToNull(serverProduces[0]); diff --git a/jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java b/jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java index 7a573e00a4..9a16e6c9c7 100644 --- a/jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java +++ b/jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java @@ -342,4 +342,34 @@ interface HeaderParams { public void emptyHeaderParam() throws Exception { contract.parseAndValidatateMetadata(HeaderParams.class.getDeclaredMethod("emptyHeaderParam", String.class)); } + + @Path("base") + interface PathsWithoutAnySlashes { + @GET @Path("specific") Response get(); + } + + @Test public void pathsWithoutSlashesParseCorrectly() throws Exception { + MethodMetadata md = contract.parseAndValidatateMetadata(PathsWithoutAnySlashes.class.getDeclaredMethod("get")); + assertEquals(md.template().url(), "/base/specific"); + } + + @Path("/base") + interface PathsWithSomeSlashes { + @GET @Path("specific") Response get(); + } + + @Test public void pathsWithSomeSlashesParseCorrectly() throws Exception { + MethodMetadata md = contract.parseAndValidatateMetadata(PathsWithSomeSlashes.class.getDeclaredMethod("get")); + assertEquals(md.template().url(), "/base/specific"); + } + + @Path("base") + interface PathsWithSomeOtherSlashes { + @GET @Path("/specific") Response get(); + } + + @Test public void pathsWithSomeOtherSlashesParseCorrectly() throws Exception { + MethodMetadata md = contract.parseAndValidatateMetadata(PathsWithSomeOtherSlashes.class.getDeclaredMethod("get")); + assertEquals(md.template().url(), "/base/specific"); + } } From 3ced4a5283f39c6028ba22202ed319f0d89b95c6 Mon Sep 17 00:00:00 2001 From: "Whitaker, Greg" Date: Mon, 21 Jul 2014 16:23:30 -0700 Subject: [PATCH 136/179] Added support for JAXB --- CHANGES.md | 1 + README.md | 12 + build.gradle | 14 ++ jaxb/README.md | 26 +++ .../java/feign/jaxb/JAXBContextFactory.java | 128 ++++++++++ .../src/main/java/feign/jaxb/JAXBDecoder.java | 70 ++++++ .../src/main/java/feign/jaxb/JAXBEncoder.java | 66 ++++++ jaxb/src/main/java/feign/jaxb/JAXBModule.java | 66 ++++++ .../feign/jaxb/JAXBContextFactoryTest.java | 76 ++++++ .../test/java/feign/jaxb/JAXBModuleTest.java | 219 ++++++++++++++++++ .../jaxb/examples/AWSSignatureVersion4.java | 163 +++++++++++++ .../java/feign/jaxb/examples/IAMExample.java | 192 +++++++++++++++ .../feign/jaxb/examples/package-info.java | 17 ++ settings.gradle | 2 +- 14 files changed, 1051 insertions(+), 1 deletion(-) create mode 100644 jaxb/README.md create mode 100644 jaxb/src/main/java/feign/jaxb/JAXBContextFactory.java create mode 100644 jaxb/src/main/java/feign/jaxb/JAXBDecoder.java create mode 100644 jaxb/src/main/java/feign/jaxb/JAXBEncoder.java create mode 100644 jaxb/src/main/java/feign/jaxb/JAXBModule.java create mode 100644 jaxb/src/test/java/feign/jaxb/JAXBContextFactoryTest.java create mode 100644 jaxb/src/test/java/feign/jaxb/JAXBModuleTest.java create mode 100644 jaxb/src/test/java/feign/jaxb/examples/AWSSignatureVersion4.java create mode 100644 jaxb/src/test/java/feign/jaxb/examples/IAMExample.java create mode 100644 jaxb/src/test/java/feign/jaxb/examples/package-info.java diff --git a/CHANGES.md b/CHANGES.md index 773413dbbf..8d8a96a17d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ ### Version 7.0 * Expose reflective dispatch hook: InvocationHandlerFactory +* Add JAXB integration ### Version 6.1.1 * Fix for #85 diff --git a/README.md b/README.md index 281f5ab156..64ec28bbe9 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,18 @@ api = Feign.builder() .target(Api.class, "https://apihost"); ``` +### JAXB +[JAXBModule](https://github.com/Netflix/feign/tree/master/jaxb) allows you to encode and decode XML using JAXB. + +Add `JAXBEncoder` and/or `JAXBDecoder` to your `Feign.Builder` like so: + +```java +api = Feign.builder() + .encoder(new JAXBEncoder()) + .decoder(new JAXBDecoder()) + .target(Api.class, "https://apihost"); +``` + ### JAX-RS [JAXRSModule](https://github.com/Netflix/feign/tree/master/jaxrs) overrides annotation processing to instead use standard ones supplied by the JAX-RS specification. This is currently targeted at the 1.1 spec. diff --git a/build.gradle b/build.gradle index 9e11a60276..39272bb145 100644 --- a/build.gradle +++ b/build.gradle @@ -95,6 +95,20 @@ project(':feign-jackson') { } } +project(':feign-jaxb') { + apply plugin: 'java' + + test { + useTestNG() + } + + dependencies { + compile project(':feign-core') + testCompile 'org.testng:testng:6.8.5' + testCompile 'com.google.guava:guava:14.0.1' + } +} + project(':feign-jaxrs') { apply plugin: 'java' diff --git a/jaxb/README.md b/jaxb/README.md new file mode 100644 index 0000000000..46e1e2d7a4 --- /dev/null +++ b/jaxb/README.md @@ -0,0 +1,26 @@ +JAXB Codec +=================== + +This module adds support for encoding and decoding XML via JAXB. + +Add `JAXBEncoder` and/or `JAXBDecoder` to your `Feign.Builder` like so: + +```java +JAXBContextFactory jaxbFactory = new JAXBContextFactory.Builder() + .withMarshallerJAXBEncoding("UTF-8") + .withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd") + .build(); + +Response response = Feign.builder() + .encoder(new JAXBEncoder(jaxbFactory)) + .decoder(new JAXBDecoder(jaxbFactory)) + .target(Response.class, "https://apihost"); +``` + +Alternatively, you can add the encoder and decoder to your Dagger object graph using the provided JAXBModule like so: + +```java +JAXBContextFactory jaxbFactory = new JAXBContextFactory.Builder().build(); + +Response response = Feign.create(Response.class, "https://apihost", new JAXBModule(jaxbFactory)); +``` \ No newline at end of file diff --git a/jaxb/src/main/java/feign/jaxb/JAXBContextFactory.java b/jaxb/src/main/java/feign/jaxb/JAXBContextFactory.java new file mode 100644 index 0000000000..3929325972 --- /dev/null +++ b/jaxb/src/main/java/feign/jaxb/JAXBContextFactory.java @@ -0,0 +1,128 @@ +/* + * Copyright 2014 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.jaxb; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.PropertyException; +import javax.xml.bind.Unmarshaller; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Creates and caches JAXB contexts as well as creates Marshallers and Unmarshallers for each context. + */ +public final class JAXBContextFactory { + private final ConcurrentHashMap jaxbContexts = new ConcurrentHashMap(64); + private final Map properties; + + private JAXBContextFactory(Map properties) { + this.properties = properties; + } + + /** + * Creates a new {@link javax.xml.bind.Unmarshaller} that handles the supplied class. + */ + public Unmarshaller createUnmarshaller(Class clazz) throws JAXBException { + JAXBContext ctx = getContext(clazz); + return ctx.createUnmarshaller(); + } + + /** + * Creates a new {@link javax.xml.bind.Marshaller} that handles the supplied class. + */ + public Marshaller createMarshaller(Class clazz) throws JAXBException { + JAXBContext ctx = getContext(clazz); + Marshaller marshaller = ctx.createMarshaller(); + setMarshallerProperties(marshaller); + return marshaller; + } + + private void setMarshallerProperties(Marshaller marshaller) throws PropertyException { + Iterator keys = properties.keySet().iterator(); + + while(keys.hasNext()) { + String key = keys.next(); + marshaller.setProperty(key, properties.get(key)); + } + } + + private JAXBContext getContext(Class clazz) throws JAXBException { + JAXBContext jaxbContext = this.jaxbContexts.get(clazz); + if (jaxbContext == null) { + jaxbContext = JAXBContext.newInstance(clazz); + this.jaxbContexts.putIfAbsent(clazz, jaxbContext); + } + return jaxbContext; + } + + /** + * Creates instances of {@link feign.jaxb.JAXBContextFactory} + */ + public static class Builder { + private final Map properties = new HashMap(5); + + /** + * Sets the jaxb.encoding property of any Marshaller created by this factory. + */ + public Builder withMarshallerJAXBEncoding(String value) { + properties.put(Marshaller.JAXB_ENCODING, value); + return this; + } + + /** + * Sets the jaxb.schemaLocation property of any Marshaller created by this factory. + */ + public Builder withMarshallerSchemaLocation(String value) { + properties.put(Marshaller.JAXB_SCHEMA_LOCATION, value); + return this; + } + + /** + * Sets the jaxb.noNamespaceSchemaLocation property of any Marshaller created by this factory. + */ + public Builder withMarshallerNoNamespaceSchemaLocation(String value) { + properties.put(Marshaller.JAXB_NO_NAMESPACE_SCHEMA_LOCATION, value); + return this; + } + + /** + * Sets the jaxb.formatted.output property of any Marshaller created by this factory. + */ + public Builder withMarshallerFormattedOutput(Boolean value) { + properties.put(Marshaller.JAXB_FORMATTED_OUTPUT, value); + return this; + } + + /** + * Sets the jaxb.fragment property of any Marshaller created by this factory. + */ + public Builder withMarshallerFragment(Boolean value) { + properties.put(Marshaller.JAXB_FRAGMENT, value); + return this; + } + + /** + * Creates a new {@link feign.jaxb.JAXBContextFactory} instance. + */ + public JAXBContextFactory build() { + return new JAXBContextFactory(properties); + } + } +} diff --git a/jaxb/src/main/java/feign/jaxb/JAXBDecoder.java b/jaxb/src/main/java/feign/jaxb/JAXBDecoder.java new file mode 100644 index 0000000000..b119463f28 --- /dev/null +++ b/jaxb/src/main/java/feign/jaxb/JAXBDecoder.java @@ -0,0 +1,70 @@ +/* + * Copyright 2014 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.jaxb; + +import feign.FeignException; +import feign.Response; +import feign.codec.DecodeException; +import feign.codec.Decoder; + +import javax.inject.Inject; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Unmarshaller; +import java.io.IOException; +import java.lang.reflect.Type; + +/** + * Decodes responses using JAXB. + *
+ *

+ * Basic example with with Feign.Builder: + *

+ *
+ * JAXBContextFactory jaxbFactory = new JAXBContextFactory.Builder()
+ *      .withMarshallerJAXBEncoding("UTF-8")
+ *      .withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd")
+ *      .build();
+ *
+ * api = Feign.builder()
+ *            .decoder(new JAXBDecoder(jaxbFactory))
+ *            .target(MyApi.class, "http://api");
+ * 
+ *

+ * The JAXBContextFactory should be reused across requests as it caches the created JAXB contexts. + *

+ */ +public class JAXBDecoder implements Decoder { + private final JAXBContextFactory jaxbContextFactory; + + @Inject + public JAXBDecoder(JAXBContextFactory jaxbContextFactory) { + this.jaxbContextFactory = jaxbContextFactory; + } + + @Override + public Object decode(Response response, Type type) throws IOException, FeignException { + try { + Unmarshaller unmarshaller = jaxbContextFactory.createUnmarshaller((Class) type); + return unmarshaller.unmarshal(response.body().asInputStream()); + } catch (JAXBException e) { + throw new DecodeException(e.toString(), e); + } finally { + if(response.body() != null) { + response.body().close(); + } + } + } +} diff --git a/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java b/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java new file mode 100644 index 0000000000..acbf0ca34f --- /dev/null +++ b/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java @@ -0,0 +1,66 @@ +/* + * Copyright 2014 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.jaxb; + +import feign.RequestTemplate; +import feign.codec.EncodeException; +import feign.codec.Encoder; + +import javax.inject.Inject; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import java.io.StringWriter; + +/** + * Encodes requests using JAXB. + *
+ *

+ * Basic example with with Feign.Builder: + *

+ *
+ * JAXBContextFactory jaxbFactory = new JAXBContextFactory.Builder()
+ *      .withMarshallerJAXBEncoding("UTF-8")
+ *      .withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd")
+ *      .build();
+ *
+ * api = Feign.builder()
+ *            .encoder(new JAXBEncoder(jaxbFactory))
+ *            .target(MyApi.class, "http://api");
+ * 
+ *

+ * The JAXBContextFactory should be reused across requests as it caches the created JAXB contexts. + *

+ */ +public class JAXBEncoder implements Encoder { + private final JAXBContextFactory jaxbContextFactory; + + @Inject + public JAXBEncoder(JAXBContextFactory jaxbContextFactory) { + this.jaxbContextFactory = jaxbContextFactory; + } + + @Override + public void encode(Object object, RequestTemplate template) throws EncodeException { + try { + Marshaller marshaller = jaxbContextFactory.createMarshaller(object.getClass()); + StringWriter stringWriter = new StringWriter(); + marshaller.marshal(object, stringWriter); + template.body(stringWriter.toString()); + } catch (JAXBException e) { + throw new EncodeException(e.toString(), e); + } + } +} diff --git a/jaxb/src/main/java/feign/jaxb/JAXBModule.java b/jaxb/src/main/java/feign/jaxb/JAXBModule.java new file mode 100644 index 0000000000..94835dfef0 --- /dev/null +++ b/jaxb/src/main/java/feign/jaxb/JAXBModule.java @@ -0,0 +1,66 @@ +/* + * Copyright 2014 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.jaxb; + +import dagger.Provides; +import feign.Feign; +import feign.codec.Decoder; +import feign.codec.Encoder; + +import javax.inject.Singleton; + +/** + * Provides an Encoder and Decoder for handling XML responses with JAXB annotated classes. + *

+ *
+ * Here is an example of configuring a custom JAXBContextFactory: + *

+ *
+ *    JAXBContextFactory jaxbFactory = new JAXBContextFactory.Builder()
+ *               .withMarshallerJAXBEncoding("UTF-8")
+ *               .withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd")
+ *               .build();
+ *
+ *    Response response = Feign.create(Response.class, "http://apihost", new JAXBModule(jaxbFactory));
+ * 
+ *

+ * The JAXBContextFactory should be reused across requests as it caches the created JAXB contexts. + *

+ */ +@dagger.Module(injects = Feign.class, addsTo = Feign.Defaults.class) +public final class JAXBModule { + private final JAXBContextFactory jaxbContextFactory; + + public JAXBModule() { + this.jaxbContextFactory = new JAXBContextFactory.Builder().build(); + } + + public JAXBModule(JAXBContextFactory jaxbContextFactory) { + this.jaxbContextFactory = jaxbContextFactory; + } + + @Provides Encoder encoder(JAXBEncoder jaxbEncoder) { + return jaxbEncoder; + } + + @Provides Decoder decoder(JAXBDecoder jaxbDecoder) { + return jaxbDecoder; + } + + @Provides @Singleton JAXBContextFactory jaxbContextFactory() { + return this.jaxbContextFactory; + } +} diff --git a/jaxb/src/test/java/feign/jaxb/JAXBContextFactoryTest.java b/jaxb/src/test/java/feign/jaxb/JAXBContextFactoryTest.java new file mode 100644 index 0000000000..b7544cc0fe --- /dev/null +++ b/jaxb/src/test/java/feign/jaxb/JAXBContextFactoryTest.java @@ -0,0 +1,76 @@ +/* + * Copyright 2014 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.jaxb; + +import org.testng.annotations.Test; + +import javax.xml.bind.Marshaller; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +public class JAXBContextFactoryTest { + @Test + public void buildsMarshallerWithJAXBEncodingProperty() throws Exception { + JAXBContextFactory factory = new JAXBContextFactory.Builder() + .withMarshallerJAXBEncoding("UTF-16") + .build(); + + Marshaller marshaller = factory.createMarshaller(Object.class); + assertEquals(marshaller.getProperty(Marshaller.JAXB_ENCODING), "UTF-16"); + } + + @Test + public void buildsMarshallerWithSchemaLocationProperty() throws Exception { + JAXBContextFactory factory = new JAXBContextFactory.Builder() + .withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd") + .build(); + + Marshaller marshaller = factory.createMarshaller(Object.class); + assertEquals(marshaller.getProperty(Marshaller.JAXB_SCHEMA_LOCATION), + "http://apihost http://apihost/schema.xsd"); + } + + @Test + public void buildsMarshallerWithNoNamespaceSchemaLocationProperty() throws Exception { + JAXBContextFactory factory = new JAXBContextFactory.Builder() + .withMarshallerNoNamespaceSchemaLocation("http://apihost/schema.xsd") + .build(); + + Marshaller marshaller = factory.createMarshaller(Object.class); + assertEquals(marshaller.getProperty(Marshaller.JAXB_NO_NAMESPACE_SCHEMA_LOCATION), "http://apihost/schema.xsd"); + } + + @Test + public void buildsMarshallerWithFormattedOutputProperty() throws Exception { + JAXBContextFactory factory = new JAXBContextFactory.Builder() + .withMarshallerFormattedOutput(true) + .build(); + + Marshaller marshaller = factory.createMarshaller(Object.class); + assertTrue((Boolean) marshaller.getProperty(Marshaller.JAXB_FORMATTED_OUTPUT)); + } + + @Test + public void buildsMarshallerWithFragmentProperty() throws Exception { + JAXBContextFactory factory = new JAXBContextFactory.Builder() + .withMarshallerFragment(true) + .build(); + + Marshaller marshaller = factory.createMarshaller(Object.class); + assertTrue((Boolean) marshaller.getProperty(Marshaller.JAXB_FRAGMENT)); + } +} diff --git a/jaxb/src/test/java/feign/jaxb/JAXBModuleTest.java b/jaxb/src/test/java/feign/jaxb/JAXBModuleTest.java new file mode 100644 index 0000000000..104d66d080 --- /dev/null +++ b/jaxb/src/test/java/feign/jaxb/JAXBModuleTest.java @@ -0,0 +1,219 @@ +/* + * Copyright 2014 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.jaxb; + +import com.google.common.reflect.TypeToken; +import dagger.Module; +import dagger.ObjectGraph; +import feign.RequestTemplate; +import feign.Response; +import feign.codec.Decoder; +import feign.codec.Encoder; +import org.testng.annotations.Test; + +import javax.inject.Inject; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.Collection; +import java.util.Collections; + +import static feign.Util.UTF_8; +import static org.testng.Assert.assertEquals; + +@Test +public class JAXBModuleTest { + @Module(includes = JAXBModule.class, injects = EncoderAndDecoderBindings.class) + static class EncoderAndDecoderBindings { + @Inject + Encoder encoder; + + @Inject + Decoder decoder; + } + + @Module(includes = JAXBModule.class, injects = EncoderBindings.class) + static class EncoderBindings { + @Inject Encoder encoder; + } + + @Module(includes = JAXBModule.class, injects = DecoderBindings.class) + static class DecoderBindings { + @Inject Decoder decoder; + } + + @Test + public void providesEncoderDecoder() throws Exception { + EncoderAndDecoderBindings bindings = new EncoderAndDecoderBindings(); + ObjectGraph.create(bindings).inject(bindings); + + assertEquals(bindings.encoder.getClass(), JAXBEncoder.class); + assertEquals(bindings.decoder.getClass(), JAXBDecoder.class); + } + + @XmlRootElement + @XmlAccessorType(XmlAccessType.FIELD) + static class MockObject { + + @XmlElement + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + MockObject that = (MockObject) o; + + if (value != null ? !value.equals(that.value) : that.value != null) return false; + + return true; + } + + @Override + public int hashCode() { + return value != null ? value.hashCode() : 0; + } + } + + @Test + public void encodesXml() throws Exception { + EncoderBindings bindings = new EncoderBindings(); + ObjectGraph.create(bindings).inject(bindings); + + MockObject mock = new MockObject(); + mock.setValue("Test"); + + RequestTemplate template = new RequestTemplate(); + bindings.encoder.encode(mock, template); + + assertEquals(new String(template.body(), UTF_8), "Test"); + } + + @Test + public void encodesXmlWithCustomJAXBEncoding() throws Exception { + JAXBContextFactory jaxbContextFactory = new JAXBContextFactory.Builder() + .withMarshallerJAXBEncoding("UTF-16") + .build(); + + JAXBModule jaxbModule = new JAXBModule(jaxbContextFactory); + Encoder encoder = jaxbModule.encoder(new JAXBEncoder(jaxbContextFactory)); + + MockObject mock = new MockObject(); + mock.setValue("Test"); + + RequestTemplate template = new RequestTemplate(); + encoder.encode(mock, template); + + assertEquals(new String(template.body(), UTF_8), "Test"); + } + + @Test + public void encodesXmlWithCustomJAXBSchemaLocation() throws Exception { + JAXBContextFactory jaxbContextFactory = new JAXBContextFactory.Builder() + .withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd") + .build(); + + JAXBModule jaxbModule = new JAXBModule(jaxbContextFactory); + Encoder encoder = jaxbModule.encoder(new JAXBEncoder(jaxbContextFactory)); + + MockObject mock = new MockObject(); + mock.setValue("Test"); + + RequestTemplate template = new RequestTemplate(); + encoder.encode(mock, template); + + assertEquals(new String(template.body(), UTF_8), "" + + "Test"); + } + + @Test + public void encodesXmlWithCustomJAXBNoNamespaceSchemaLocation() throws Exception { + JAXBContextFactory jaxbContextFactory = new JAXBContextFactory.Builder() + .withMarshallerNoNamespaceSchemaLocation("http://apihost/schema.xsd") + .build(); + + JAXBModule jaxbModule = new JAXBModule(jaxbContextFactory); + Encoder encoder = jaxbModule.encoder(new JAXBEncoder(jaxbContextFactory)); + + MockObject mock = new MockObject(); + mock.setValue("Test"); + + RequestTemplate template = new RequestTemplate(); + encoder.encode(mock, template); + + assertEquals(new String(template.body(), UTF_8), "" + + "Test"); + } + + @Test + public void encodesXmlWithCustomJAXBFormattedOutput() { + JAXBContextFactory jaxbContextFactory = new JAXBContextFactory.Builder() + .withMarshallerFormattedOutput(true) + .build(); + + JAXBModule jaxbModule = new JAXBModule(jaxbContextFactory); + Encoder encoder = jaxbModule.encoder(new JAXBEncoder(jaxbContextFactory)); + + MockObject mock = new MockObject(); + mock.setValue("Test"); + + RequestTemplate template = new RequestTemplate(); + encoder.encode(mock, template); + + String NEWLINE = System.getProperty("line.separator"); + + StringBuilder expectedXml = new StringBuilder(); + expectedXml.append("").append(NEWLINE) + .append("").append(NEWLINE) + .append(" Test").append(NEWLINE) + .append("").append(NEWLINE); + + assertEquals(new String(template.body(), UTF_8), expectedXml.toString()); + } + + @Test + public void decodesXml() throws Exception { + DecoderBindings bindings = new DecoderBindings(); + ObjectGraph.create(bindings).inject(bindings); + + MockObject mock = new MockObject(); + mock.setValue("Test"); + + String mockXml = "" + + "Test"; + + Response response = + Response.create(200, "OK", Collections.>emptyMap(), mockXml, UTF_8); + + assertEquals(bindings.decoder.decode(response, new TypeToken() {}.getType()), mock); + } +} diff --git a/jaxb/src/test/java/feign/jaxb/examples/AWSSignatureVersion4.java b/jaxb/src/test/java/feign/jaxb/examples/AWSSignatureVersion4.java new file mode 100644 index 0000000000..0d9e3b84b5 --- /dev/null +++ b/jaxb/src/test/java/feign/jaxb/examples/AWSSignatureVersion4.java @@ -0,0 +1,163 @@ +/* + * Copyright 2014 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.jaxb.examples; + +import com.google.common.base.Function; +import com.google.common.base.Joiner; +import com.google.common.collect.Multimap; +import com.google.common.collect.TreeMultimap; +import feign.Request; +import feign.RequestTemplate; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.net.URI; +import java.text.SimpleDateFormat; +import java.util.Collection; +import java.util.Date; +import java.util.Map.Entry; +import java.util.TimeZone; + +import static com.google.common.base.Throwables.propagate; +import static com.google.common.collect.Iterables.transform; +import static com.google.common.hash.Hashing.sha256; +import static com.google.common.io.BaseEncoding.base16; +import static feign.Util.UTF_8; + +// http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html +public class AWSSignatureVersion4 implements Function { + + String region = "us-east-1"; + String service = "iam"; + String accessKey; + String secretKey; + + public AWSSignatureVersion4(String accessKey, String secretKey) { + this.accessKey = accessKey; + this.secretKey = secretKey; + } + + @Override public Request apply(RequestTemplate input) { + input.header("Host", URI.create(input.url()).getHost()); + TreeMultimap sortedLowercaseHeaders = TreeMultimap.create(); + for (String key : input.headers().keySet()) { + sortedLowercaseHeaders.putAll(trimToLowercase.apply(key), + transform(input.headers().get(key), trimToLowercase)); + } + + String timestamp; + synchronized (iso8601) { + timestamp = iso8601.format(new Date()); + } + + String credentialScope = Joiner.on('/').join(timestamp.substring(0, 8), region, service, "aws4_request"); + + input.query("X-Amz-Algorithm", "AWS4-HMAC-SHA256"); + input.query("X-Amz-Credential", accessKey + "/" + credentialScope); + input.query("X-Amz-Date", timestamp); + input.query("X-Amz-SignedHeaders", Joiner.on(';').join(sortedLowercaseHeaders.keySet())); + + String canonicalString = canonicalString(input, sortedLowercaseHeaders); + String toSign = toSign(timestamp, credentialScope, canonicalString); + + byte[] signatureKey = signatureKey(secretKey, timestamp); + String signature = base16().lowerCase().encode(hmacSHA256(toSign, signatureKey)); + + input.query("X-Amz-Signature", signature); + + return input.request(); + } + + byte[] signatureKey(String secretKey, String timestamp) { + byte[] kSecret = ("AWS4" + secretKey).getBytes(UTF_8); + byte[] kDate = hmacSHA256(timestamp.substring(0, 8), kSecret); + byte[] kRegion = hmacSHA256(region, kDate); + byte[] kService = hmacSHA256(service, kRegion); + byte[] kSigning = hmacSHA256("aws4_request", kService); + return kSigning; + } + + static byte[] hmacSHA256(String data, byte[] key) { + try { + String algorithm = "HmacSHA256"; + Mac mac = Mac.getInstance(algorithm); + mac.init(new SecretKeySpec(key, algorithm)); + return mac.doFinal(data.getBytes(UTF_8)); + } catch (Exception e) { + throw propagate(e); + } + } + + private static final String EMPTY_STRING_HASH = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + + private String canonicalString(RequestTemplate input, Multimap sortedLowercaseHeaders) { + StringBuilder canonicalRequest = new StringBuilder(); + // HTTPRequestMethod + '\n' + + canonicalRequest.append(input.method()).append('\n'); + + // CanonicalURI + '\n' + + canonicalRequest.append(URI.create(input.url()).getPath()).append('\n'); + + // CanonicalQueryString + '\n' + + canonicalRequest.append(input.queryLine().substring(1)); + canonicalRequest.append('\n'); + + // CanonicalHeaders + '\n' + + for (Entry> entry : sortedLowercaseHeaders.asMap().entrySet()) { + canonicalRequest.append(entry.getKey()).append(':').append(Joiner.on(',').join(entry.getValue())) + .append('\n'); + } + canonicalRequest.append('\n'); + + // SignedHeaders + '\n' + + canonicalRequest.append(Joiner.on(',').join(sortedLowercaseHeaders.keySet())).append('\n'); + + // HexEncode(Hash(Payload)) + String bodyText = + input.charset() != null && input.body() != null ? new String(input.body(), input.charset()) : null; + if (bodyText != null) { + canonicalRequest.append(base16().lowerCase().encode(sha256().hashString(bodyText, UTF_8).asBytes())); + } else { + canonicalRequest.append(EMPTY_STRING_HASH); + } + return canonicalRequest.toString(); + } + + private static final Function trimToLowercase = new Function() { + public String apply(String in) { + return in == null ? null : in.toLowerCase().trim(); + } + }; + + private String toSign(String timestamp, String credentialScope, String canonicalRequest) { + StringBuilder toSign = new StringBuilder(); + // Algorithm + '\n' + + toSign.append("AWS4-HMAC-SHA256").append('\n'); + // RequestDate + '\n' + + toSign.append(timestamp).append('\n'); + // CredentialScope + '\n' + + toSign.append(credentialScope).append('\n'); + // HexEncode(Hash(CanonicalRequest)) + toSign.append(base16().lowerCase().encode(sha256().hashString(canonicalRequest, UTF_8).asBytes())); + return toSign.toString(); + } + + private static final SimpleDateFormat iso8601 = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"); + + static { + iso8601.setTimeZone(TimeZone.getTimeZone("GMT")); + } +} diff --git a/jaxb/src/test/java/feign/jaxb/examples/IAMExample.java b/jaxb/src/test/java/feign/jaxb/examples/IAMExample.java new file mode 100644 index 0000000000..dd661017c2 --- /dev/null +++ b/jaxb/src/test/java/feign/jaxb/examples/IAMExample.java @@ -0,0 +1,192 @@ +/* + * Copyright 2014 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.jaxb.examples; + +import feign.Feign; +import feign.Request; +import feign.RequestLine; +import feign.RequestTemplate; +import feign.Target; +import feign.jaxb.JAXBContextFactory; +import feign.jaxb.JAXBDecoder; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlType; + +public class IAMExample { + + interface IAM { + @RequestLine("GET /?Action=GetUser&Version=2010-05-08") GetUserResponse userResponse(); + } + + public static void main(String... args) { + IAM iam = Feign.builder() + .decoder(new JAXBDecoder(new JAXBContextFactory.Builder().build())) + .target(new IAMTarget(args[0], args[1])); + + GetUserResponse response = iam.userResponse(); + System.out.println("UserId: " + response.getUserResult().getUser().getUserId()); + System.out.println("UserName: " + response.getUserResult().getUser().getUsername()); + } + + static class IAMTarget extends AWSSignatureVersion4 implements Target { + + @Override public Class 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); + } + } + + @XmlRootElement(name = "GetUserResponse", namespace = "https://iam.amazonaws.com/doc/2010-05-08/") + @XmlAccessorType(XmlAccessType.FIELD) + static class GetUserResponse { + @XmlElement(name = "GetUserResult") + private GetUserResult userResult; + + @XmlElement(name = "ResponseMetadata") + private ResponseMetadata responseMetadata; + + public GetUserResult getUserResult() { + return userResult; + } + + public void setUserResult(GetUserResult userResult) { + this.userResult = userResult; + } + + public ResponseMetadata getResponseMetadata() { + return responseMetadata; + } + + public void setResponseMetadata(ResponseMetadata responseMetadata) { + this.responseMetadata = responseMetadata; + } + } + + @XmlAccessorType(XmlAccessType.FIELD) + @XmlType(name = "ResponseMetadata") + static class ResponseMetadata { + @XmlElement(name = "RequestId") + private String requestId; + + public ResponseMetadata() {} + + public String getRequestId() { + return requestId; + } + + public void setRequestId(String requestId) { + this.requestId = requestId; + } + } + + @XmlAccessorType(XmlAccessType.FIELD) + @XmlType(name = "GetUserResult") + static class GetUserResult { + @XmlElement(name = "User") + private User user; + + public GetUserResult() {} + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + } + + @XmlAccessorType(XmlAccessType.FIELD) + @XmlType(name = "User") + static class User { + @XmlElement(name = "UserId") + private String userId; + + @XmlElement(name = "Path") + private String path; + + @XmlElement(name = "UserName") + private String username; + + @XmlElement(name = "Arn") + private String arn; + + @XmlElement(name = "CreateDate") + private String createDate; + + public User() {} + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getArn() { + return arn; + } + + public void setArn(String arn) { + this.arn = arn; + } + + public String getCreateDate() { + return createDate; + } + + public void setCreateDate(String createDate) { + this.createDate = createDate; + } + } +} diff --git a/jaxb/src/test/java/feign/jaxb/examples/package-info.java b/jaxb/src/test/java/feign/jaxb/examples/package-info.java new file mode 100644 index 0000000000..0038947aa9 --- /dev/null +++ b/jaxb/src/test/java/feign/jaxb/examples/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright 2014 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. + */ +@javax.xml.bind.annotation.XmlSchema(namespace = "https://iam.amazonaws.com/doc/2010-05-08/", elementFormDefault = javax.xml.bind.annotation.XmlNsForm.QUALIFIED) +package feign.jaxb.examples; diff --git a/settings.gradle b/settings.gradle index 89c278128f..6f6dc626f5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,5 @@ rootProject.name='feign' -include 'core', 'sax', 'gson', 'jackson', 'jaxrs', 'ribbon', 'slf4j', 'example-github', 'example-wikipedia' +include 'core', 'sax', 'gson', 'jackson', 'jaxb', 'jaxrs', 'ribbon', 'slf4j', 'example-github', 'example-wikipedia' rootProject.children.each { childProject -> childProject.name = 'feign-' + childProject.name From 0023ae63af4e29ed8e5960497e381ab7aa55f34f Mon Sep 17 00:00:00 2001 From: Amit Joshi Date: Thu, 14 Aug 2014 16:09:59 -0700 Subject: [PATCH 137/179] Integrate with latest ribbon release (ribbon-loadbalancer) --- build.gradle | 2 +- .../src/main/java/feign/ribbon/LBClient.java | 66 ++++++++++--------- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/build.gradle b/build.gradle index 9e11a60276..f0ee4bedef 100644 --- a/build.gradle +++ b/build.gradle @@ -120,7 +120,7 @@ project(':feign-ribbon') { dependencies { compile project(':feign-core') - compile 'com.netflix.ribbon:ribbon-core:0.2.4' + compile 'com.netflix.ribbon:ribbon-loadbalancer:2.0-RC5' testCompile 'org.testng:testng:6.8.5' testCompile 'com.google.mockwebserver:mockwebserver:20130706' } diff --git a/ribbon/src/main/java/feign/ribbon/LBClient.java b/ribbon/src/main/java/feign/ribbon/LBClient.java index a6d79205a2..078b8083fc 100644 --- a/ribbon/src/main/java/feign/ribbon/LBClient.java +++ b/ribbon/src/main/java/feign/ribbon/LBClient.java @@ -19,10 +19,10 @@ import com.netflix.client.ClientException; import com.netflix.client.ClientRequest; import com.netflix.client.IResponse; +import com.netflix.client.RequestSpecificRetryHandler; import com.netflix.client.config.CommonClientConfigKey; import com.netflix.client.config.IClientConfig; import com.netflix.loadbalancer.ILoadBalancer; -import com.netflix.util.Pair; import java.io.IOException; import java.net.URI; @@ -35,9 +35,6 @@ import feign.Response; import feign.RetryableException; -import static com.netflix.client.config.CommonClientConfigKey.ConnectTimeout; -import static com.netflix.client.config.CommonClientConfigKey.ReadTimeout; - class LBClient extends AbstractLoadBalancerAwareClient { private final Client delegate; @@ -45,38 +42,40 @@ class LBClient extends AbstractLoadBalancerAwareClient deriveSchemeAndPortFromPartialUri(RibbonRequest task) { - return new Pair(URI.create(task.request.url()).getScheme(), task.getUri().getPort()); - } - - @Override protected int getDefaultPort() { - return 443; + public RequestSpecificRetryHandler getRequestSpecificRetryHandler( + RibbonRequest request, IClientConfig requestConfig) { + + return new RequestSpecificRetryHandler(true, false) { + @Override + public boolean isRetriableException(Throwable e, boolean sameServer) { + return e instanceof RetryableException; + } + + @Override + public boolean isCircuitTrippingException(Throwable e) { + return e instanceof IOException; + } + }; } static class RibbonRequest extends ClientRequest implements Cloneable { @@ -135,11 +134,14 @@ static class RibbonResponse implements IResponse { Response toResponse() { return response; } - } - static int config(RibbonRequest request, CommonClientConfigKey key, int defaultValue) { - if (request.getOverrideConfig() != null && request.getOverrideConfig().containsProperty(key)) - return Integer.valueOf(request.getOverrideConfig().getProperty(key).toString()); - return defaultValue; + @Override + public void close() throws IOException { + if (response != null && response.body() != null) { + response.body().close(); + } + } + } + } From 24d70fa5efec51149c5edb510b1447e8e67f8a8e Mon Sep 17 00:00:00 2001 From: Allen Wang Date: Mon, 18 Aug 2014 10:04:48 -0700 Subject: [PATCH 138/179] Refine the retry handler logic for LBClient. --- .../src/main/java/feign/ribbon/LBClient.java | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/ribbon/src/main/java/feign/ribbon/LBClient.java b/ribbon/src/main/java/feign/ribbon/LBClient.java index 078b8083fc..76ff7035eb 100644 --- a/ribbon/src/main/java/feign/ribbon/LBClient.java +++ b/ribbon/src/main/java/feign/ribbon/LBClient.java @@ -20,29 +20,31 @@ import com.netflix.client.ClientRequest; import com.netflix.client.IResponse; import com.netflix.client.RequestSpecificRetryHandler; +import com.netflix.client.RetryHandler; import com.netflix.client.config.CommonClientConfigKey; import com.netflix.client.config.IClientConfig; import com.netflix.loadbalancer.ILoadBalancer; +import feign.Client; +import feign.Request; +import feign.RequestTemplate; +import feign.Response; import java.io.IOException; import java.net.URI; import java.util.Collection; import java.util.Map; -import feign.Client; -import feign.Request; -import feign.RequestTemplate; -import feign.Response; -import feign.RetryableException; - class LBClient extends AbstractLoadBalancerAwareClient { private final Client delegate; private final int connectTimeout; private final int readTimeout; + private final IClientConfig clientConfig; LBClient(Client delegate, ILoadBalancer lb, IClientConfig clientConfig) { super(lb, clientConfig); + this.setRetryHandler(RetryHandler.DEFAULT); + this.clientConfig = clientConfig; this.delegate = delegate; connectTimeout = clientConfig.get(CommonClientConfigKey.ConnectTimeout); readTimeout = clientConfig.get(CommonClientConfigKey.ReadTimeout); @@ -63,19 +65,18 @@ public RibbonResponse execute(RibbonRequest request, IClientConfig configOverrid @Override public RequestSpecificRetryHandler getRequestSpecificRetryHandler( - RibbonRequest request, IClientConfig requestConfig) { - - return new RequestSpecificRetryHandler(true, false) { - @Override - public boolean isRetriableException(Throwable e, boolean sameServer) { - return e instanceof RetryableException; - } - - @Override - public boolean isCircuitTrippingException(Throwable e) { - return e instanceof IOException; - } - }; + RibbonRequest request, IClientConfig requestConfig) { + if (!request.isRetriable()) { + return new RequestSpecificRetryHandler(false, false, this.getRetryHandler(), requestConfig); + } + if (clientConfig.get(CommonClientConfigKey.OkToRetryOnAllOperations, false)) { + return new RequestSpecificRetryHandler(true, true, this.getRetryHandler(), requestConfig); + } + if (!request.toRequest().method().equals("GET")) { + return new RequestSpecificRetryHandler(true, false, this.getRetryHandler(), requestConfig); + } else { + return new RequestSpecificRetryHandler(true, true, this.getRetryHandler(), requestConfig); + } } static class RibbonRequest extends ClientRequest implements Cloneable { From fc43eedbc510cf9bccb51361bf810bf5a8a41f08 Mon Sep 17 00:00:00 2001 From: Allen Wang Date: Mon, 18 Aug 2014 10:11:36 -0700 Subject: [PATCH 139/179] Remove unnecessary code --- ribbon/src/main/java/feign/ribbon/LBClient.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/ribbon/src/main/java/feign/ribbon/LBClient.java b/ribbon/src/main/java/feign/ribbon/LBClient.java index 76ff7035eb..83fd602ed6 100644 --- a/ribbon/src/main/java/feign/ribbon/LBClient.java +++ b/ribbon/src/main/java/feign/ribbon/LBClient.java @@ -66,9 +66,6 @@ public RibbonResponse execute(RibbonRequest request, IClientConfig configOverrid @Override public RequestSpecificRetryHandler getRequestSpecificRetryHandler( RibbonRequest request, IClientConfig requestConfig) { - if (!request.isRetriable()) { - return new RequestSpecificRetryHandler(false, false, this.getRetryHandler(), requestConfig); - } if (clientConfig.get(CommonClientConfigKey.OkToRetryOnAllOperations, false)) { return new RequestSpecificRetryHandler(true, true, this.getRetryHandler(), requestConfig); } From 2edd8beae0c1dabb6c2480cb7fd9a0b2e5146828 Mon Sep 17 00:00:00 2001 From: Alexander Schwartz Date: Sat, 18 Oct 2014 19:52:28 +0200 Subject: [PATCH 140/179] fixing language hint json->java --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 281f5ab156..a11ff6a2d9 100644 --- a/README.md +++ b/README.md @@ -172,7 +172,7 @@ If any methods in your interface use parameters types besides `String` or `byte[ Here's how to configure JSON encoding (using the `feign-gson` extension): -```json +```java GitHub github = Feign.builder() .encoder(new GsonEncoder()) .target(GitHub.class, "https://api.github.com"); From 35c3e4e7f505c67d7b258611dc63ce45df1c1ab9 Mon Sep 17 00:00:00 2001 From: Matthew Hall Date: Tue, 4 Nov 2014 16:11:20 -0700 Subject: [PATCH 141/179] Upgrade Dagger dependency from 1.1.0 to 1.2.2. --- dagger.gradle | 2 +- example-github/build.gradle | 2 +- example-wikipedia/build.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dagger.gradle b/dagger.gradle index 599960266f..840a216165 100644 --- a/dagger.gradle +++ b/dagger.gradle @@ -28,7 +28,7 @@ apply plugin: 'idea' if (!project.hasProperty('daggerVersion')) { ext { - daggerVersion = "1.1.0" + daggerVersion = "1.2.2" } } diff --git a/example-github/build.gradle b/example-github/build.gradle index 8b92037caf..631015a93b 100644 --- a/example-github/build.gradle +++ b/example-github/build.gradle @@ -3,7 +3,7 @@ apply plugin: 'java' dependencies { compile 'com.netflix.feign:feign-core:5.3.0' compile 'com.netflix.feign:feign-gson:5.3.0' - provided 'com.squareup.dagger:dagger-compiler:1.1.0' + provided 'com.squareup.dagger:dagger-compiler:1.2.2' } // create a self-contained jar that is executable diff --git a/example-wikipedia/build.gradle b/example-wikipedia/build.gradle index 9a85fd6165..0589c055d8 100644 --- a/example-wikipedia/build.gradle +++ b/example-wikipedia/build.gradle @@ -3,7 +3,7 @@ apply plugin: 'java' dependencies { compile 'com.netflix.feign:feign-core:5.3.0' compile 'com.netflix.feign:feign-gson:5.3.0' - provided 'com.squareup.dagger:dagger-compiler:1.1.0' + provided 'com.squareup.dagger:dagger-compiler:1.2.2' } // create a self-contained jar that is executable From 0f765ad2b8d8dba70505e398dee4f5fef77a3078 Mon Sep 17 00:00:00 2001 From: Matthew Hall Date: Tue, 4 Nov 2014 16:39:55 -0700 Subject: [PATCH 142/179] Add warning to CHANGES.md regarding Dagger 1.2.0 upgrade. --- CHANGES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 8d8a96a17d..a3121ef35a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,8 @@ ### Version 7.0 * Expose reflective dispatch hook: InvocationHandlerFactory * Add JAXB integration +* Upgrade to Dagger 1.2.2. + * **Note:** Dagger-generated code prior to version 1.2.0 is incompatible with Dagger 1.2.0 and beyond. Dagger users should upgrade Dagger to at least version 1.2.0, and recompile any dependency-injected classes. ### Version 6.1.1 * Fix for #85 From fd5e4d60bfbb5b3d2cc6cdbcb230fc27a9e79e0c Mon Sep 17 00:00:00 2001 From: Julien Roy Date: Mon, 1 Dec 2014 12:26:44 +0100 Subject: [PATCH 143/179] Make RibbonClient configurable with FeignBuilder --- .../main/java/feign/ribbon/RibbonClient.java | 73 +++++++++++++++++++ .../main/java/feign/ribbon/RibbonModule.java | 43 +---------- .../java/feign/ribbon/RibbonClientTest.java | 30 ++++++++ 3 files changed, 105 insertions(+), 41 deletions(-) create mode 100644 ribbon/src/main/java/feign/ribbon/RibbonClient.java diff --git a/ribbon/src/main/java/feign/ribbon/RibbonClient.java b/ribbon/src/main/java/feign/ribbon/RibbonClient.java new file mode 100644 index 0000000000..cfa74cfe5d --- /dev/null +++ b/ribbon/src/main/java/feign/ribbon/RibbonClient.java @@ -0,0 +1,73 @@ +package feign.ribbon; + +import com.google.common.base.Throwables; +import com.netflix.client.ClientException; +import com.netflix.client.ClientFactory; +import com.netflix.client.config.IClientConfig; +import com.netflix.loadbalancer.ILoadBalancer; + +import java.io.IOException; +import java.net.URI; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLSocketFactory; + +import feign.Client; +import feign.Request; +import feign.Response; +import dagger.Lazy; + +/** + * RibbonClient can be used in Fiegn builder to activate smart routing and resiliency capabilities provided by Ribbon. + * Ex. + *
+ * MyService api = Feign.builder.client(new RibbonClient()).target(MyService.class, "http://myAppProd");
+ * 
+ * Where {@code myAppProd} is the ribbon client name and {@code myAppProd.ribbon.listOfServers} configuration + * is set. + */ +public class RibbonClient implements Client { + + private final Client delegate; + + public RibbonClient() { + this.delegate = new Client.Default( + new Lazy() { + public SSLSocketFactory get() { + return (SSLSocketFactory)SSLSocketFactory.getDefault(); + } + }, + new Lazy() { + public HostnameVerifier get() { + return HttpsURLConnection.getDefaultHostnameVerifier(); + } + } + ); + } + + public RibbonClient(Client delegate) { + this.delegate = delegate; + } + + @Override public Response execute(Request request, Request.Options options) throws IOException { + try { + URI asUri = URI.create(request.url()); + String clientName = asUri.getHost(); + URI uriWithoutSchemeAndPort = URI.create(request.url().replace(asUri.getScheme() + "://" + asUri.getHost(), "")); + LBClient.RibbonRequest ribbonRequest = new LBClient.RibbonRequest(request, uriWithoutSchemeAndPort); + return lbClient(clientName).executeWithLoadBalancer(ribbonRequest).toResponse(); + } catch (ClientException e) { + if (e.getCause() instanceof IOException) { + throw IOException.class.cast(e.getCause()); + } + throw Throwables.propagate(e); + } + } + + private LBClient lbClient(String clientName) { + IClientConfig config = ClientFactory.getNamedConfig(clientName); + ILoadBalancer lb = ClientFactory.getNamedLoadBalancer(clientName); + return new LBClient(delegate, lb, config); + } +} diff --git a/ribbon/src/main/java/feign/ribbon/RibbonModule.java b/ribbon/src/main/java/feign/ribbon/RibbonModule.java index 5dc36aeb75..fab62b970f 100644 --- a/ribbon/src/main/java/feign/ribbon/RibbonModule.java +++ b/ribbon/src/main/java/feign/ribbon/RibbonModule.java @@ -15,12 +15,6 @@ */ package feign.ribbon; -import com.google.common.base.Throwables; -import com.netflix.client.ClientException; -import com.netflix.client.ClientFactory; -import com.netflix.client.config.IClientConfig; -import com.netflix.loadbalancer.ILoadBalancer; - import java.io.IOException; import java.net.URI; @@ -30,8 +24,6 @@ import dagger.Provides; import feign.Client; -import feign.Request; -import feign.Response; /** * Adding this module will override URL resolution of {@link feign.Client Feign's client}, @@ -55,38 +47,7 @@ public class RibbonModule { return delegate; } - @Provides @Singleton Client httpClient(RibbonClient ribbon) { - return ribbon; - } - - @Singleton - static class RibbonClient implements Client { - private final Client delegate; - - @Inject - public RibbonClient(@Named("delegate") Client delegate) { - this.delegate = delegate; - } - - @Override public Response execute(Request request, Request.Options options) throws IOException { - try { - URI asUri = URI.create(request.url()); - String clientName = asUri.getHost(); - URI uriWithoutSchemeAndPort = URI.create(request.url().replace(asUri.getScheme() + "://" + asUri.getHost(), "")); - LBClient.RibbonRequest ribbonRequest = new LBClient.RibbonRequest(request, uriWithoutSchemeAndPort); - return lbClient(clientName).executeWithLoadBalancer(ribbonRequest).toResponse(); - } catch (ClientException e) { - if (e.getCause() instanceof IOException) { - throw IOException.class.cast(e.getCause()); - } - throw Throwables.propagate(e); - } - } - - private LBClient lbClient(String clientName) { - IClientConfig config = ClientFactory.getNamedConfig(clientName); - ILoadBalancer lb = ClientFactory.getNamedLoadBalancer(clientName); - return new LBClient(delegate, lb, config); - } + @Provides @Singleton Client httpClient(@Named("delegate") Client client) { + return new RibbonClient(client); } } diff --git a/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java b/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java index fb97b8debd..42ef0e6136 100644 --- a/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java +++ b/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java @@ -19,6 +19,7 @@ import com.google.mockwebserver.MockWebServer; import com.google.mockwebserver.SocketPolicy; import dagger.Provides; +import feign.Client; import feign.Feign; import feign.RequestLine; import feign.codec.Decoder; @@ -147,6 +148,35 @@ invalid characters (ex. space). } + @Test + public void ioExceptionRetryWithBuilder() throws IOException, InterruptedException { + String client = "RibbonClientTest-ioExceptionRetryWithBuilder"; + String serverListKey = client + ".ribbon.listOfServers"; + + MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); + server.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8))); + server.play(); + + getConfigInstance().setProperty(serverListKey, hostAndPort(server.getUrl(""))); + + try { + + TestInterface api = Feign.builder(). + client(new RibbonClient()). + target(TestInterface.class, "http://" + client); + + api.post(); + + assertEquals(server.getRequestCount(), 2); + // TODO: verify ribbon stats match + // assertEquals(target.lb().getLoadBalancerStats().getSingleServerStat()) + } finally { + server.shutdown(); + getConfigInstance().clearProperty(serverListKey); + } + } + static String hostAndPort(URL url) { // our build slaves have underscores in their hostnames which aren't permitted by ribbon From c9e6f50d55278d42463376d7c367fb24d4acb63e Mon Sep 17 00:00:00 2001 From: Ralph Schaer Date: Sun, 14 Dec 2014 10:58:01 +0100 Subject: [PATCH 144/179] Link to the retrofit sample is wrong --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bb92e4c477..27d27175b0 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Feign works by processing annotations into a templatized request. Just before s ### Basics -Usage typically looks like this, an adaptation of the [canonical Retrofit sample](https://github.com/square/retrofit/blob/master/retrofit-samples/github-client/src/main/java/com/example/retrofit/GitHubClient.java). +Usage typically looks like this, an adaptation of the [canonical Retrofit sample](https://github.com/square/retrofit/blob/master/samples/github-client/src/main/java/com/example/retrofit/GitHubClient.java). ```java interface GitHub { From b5ab32353a85702d3961c9c8cc91d565abe7ee63 Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Thu, 1 Jan 2015 08:18:28 -0800 Subject: [PATCH 145/179] update README --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index a3121ef35a..f0bc1b79df 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,7 @@ ### Version 7.0 * Expose reflective dispatch hook: InvocationHandlerFactory * Add JAXB integration +* Add SLF4J integration * Upgrade to Dagger 1.2.2. * **Note:** Dagger-generated code prior to version 1.2.0 is incompatible with Dagger 1.2.0 and beyond. Dagger users should upgrade Dagger to at least version 1.2.0, and recompile any dependency-injected classes. From 2c19962f69a480630d537e0c308dcfaccda21715 Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Thu, 1 Jan 2015 08:19:47 -0800 Subject: [PATCH 146/179] 8.0.0-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 868bb9b9a2..5757a51dd1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=7.0.0-SNAPSHOT +version=8.0.0-SNAPSHOT From 61e63360db46e81cb61d51878f0e7dc1b4e9ee8b Mon Sep 17 00:00:00 2001 From: Rob Spieldenner Date: Mon, 5 Jan 2015 13:54:38 -0800 Subject: [PATCH 147/179] Upgrade gradle to 2.2.1 --- gradle/wrapper/gradle-wrapper.jar | Bin 46742 -> 51018 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index faa569a9a0eedc9ff37450fed24a7efd77a86729..c97a8bdb9088d370da7e88784a7a093b971aa23a 100644 GIT binary patch delta 44103 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 delta 40005 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 061b536b4b..5344be2730 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Apr 02 11:45:56 PDT 2013 +#Fri Jan 02 13:14:58 PST 2015 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=http\://services.gradle.org/distributions/gradle-1.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-bin.zip From 44b253e71948d34471ec08f8177277c00d3cdeb2 Mon Sep 17 00:00:00 2001 From: Rob Spieldenner Date: Mon, 5 Jan 2015 15:07:44 -0800 Subject: [PATCH 148/179] Build upgrades --- CHANGES.md => CHANGELOG.md | 0 build.gradle | 157 ++------------------------------- codequality/HEADER | 13 --- core/build.gradle | 15 ++++ dagger.gradle | 4 +- example-github/build.gradle | 6 ++ example-wikipedia/build.gradle | 6 ++ gradle.properties | 1 - gradle/buildscript.gradle | 11 --- gradle/check.gradle | 26 ------ gradle/convention.gradle | 101 --------------------- gradle/license.gradle | 10 --- gradle/maven.gradle | 70 --------------- gradle/netflix-oss.gradle | 1 - gradle/release.gradle | 61 ------------- gson/build.gradle | 13 +++ jackson/build.gradle | 14 +++ jaxb/build.gradle | 11 +++ jaxrs/build.gradle | 15 ++++ ribbon/build.gradle | 14 +++ sax/build.gradle | 13 +++ slf4j/build.gradle | 12 +++ 22 files changed, 130 insertions(+), 444 deletions(-) rename CHANGES.md => CHANGELOG.md (100%) delete mode 100644 codequality/HEADER create mode 100644 core/build.gradle delete mode 100644 gradle/buildscript.gradle delete mode 100644 gradle/check.gradle delete mode 100644 gradle/convention.gradle delete mode 100644 gradle/license.gradle delete mode 100644 gradle/maven.gradle delete mode 100644 gradle/netflix-oss.gradle delete mode 100644 gradle/release.gradle create mode 100644 gson/build.gradle create mode 100644 jackson/build.gradle create mode 100644 jaxb/build.gradle create mode 100644 jaxrs/build.gradle create mode 100644 ribbon/build.gradle create mode 100644 sax/build.gradle create mode 100644 slf4j/build.gradle diff --git a/CHANGES.md b/CHANGELOG.md similarity index 100% rename from CHANGES.md rename to CHANGELOG.md diff --git a/build.gradle b/build.gradle index 165aadc16f..1c48a3fb11 100644 --- a/build.gradle +++ b/build.gradle @@ -1,156 +1,17 @@ -// Establish version and status -ext.githubProjectName = rootProject.name // Change if github project name is not the same as the root project's name - -buildscript { - repositories { - mavenLocal() - mavenCentral() - } - apply from: file('gradle/buildscript.gradle'), to: buildscript -} - -allprojects { - if (JavaVersion.current().isJava8Compatible()) { - tasks.withType(Javadoc) { - options.addStringOption('Xdoclint:none', '-quiet') // Doclint is onerous in Java 8. - } - } - repositories { - mavenLocal() - mavenCentral() - maven { url 'https://oss.sonatype.org/content/repositories/releases/' } - } +plugins { + id 'nebula.netflixoss' version '2.2.2' } -apply from: file('gradle/convention.gradle') -apply from: file('gradle/maven.gradle') -if (!JavaVersion.current().isJava8Compatible()) { - apply from: file('gradle/check.gradle') // FindBugs is incompatible with Java 8. +ext { + githubProjectName = rootProject.name // Change if github project name is not the same as the root project's name } -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 -} - -project(':feign-core') { - apply plugin: 'java' - - test { - useTestNG() - } - - dependencies { - 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.5' - testCompile 'com.google.mockwebserver:mockwebserver:20130706' - } -} - -project(':feign-sax') { - apply plugin: 'java' - - test { - useTestNG() - } - - dependencies { - compile project(':feign-core') - testCompile 'com.google.guava:guava:14.0.1' - testCompile 'org.testng:testng:6.8.5' - } -} - -project(':feign-gson') { - apply plugin: 'java' - - test { - useTestNG() - } - - dependencies { - compile project(':feign-core') - compile 'com.google.code.gson:gson:2.2.4' - testCompile 'org.testng:testng:6.8.5' - } -} - -project(':feign-jackson') { - apply plugin: 'java' + apply plugin: 'nebula.netflixoss' - test { - useTestNG() - } - - dependencies { - compile project(':feign-core') - compile 'com.fasterxml.jackson.core:jackson-databind:2.2.2' - testCompile 'org.testng:testng:6.8.5' - testCompile 'com.google.guava:guava:14.0.1' - } -} - -project(':feign-jaxb') { - apply plugin: 'java' - - test { - useTestNG() - } - - dependencies { - compile project(':feign-core') - testCompile 'org.testng:testng:6.8.5' - testCompile 'com.google.guava:guava:14.0.1' - } -} - -project(':feign-jaxrs') { - apply plugin: 'java' - - test { - useTestNG() - } - - dependencies { - compile project(':feign-core') - compile 'javax.ws.rs:jsr311-api:1.1.1' - testCompile project(':feign-gson') - testCompile 'com.google.guava:guava:14.0.1' - testCompile 'org.testng:testng:6.8.5' - } -} - -project(':feign-ribbon') { - apply plugin: 'java' - - test { - useTestNG() - } - - dependencies { - compile project(':feign-core') - compile 'com.netflix.ribbon:ribbon-loadbalancer:2.0-RC5' - testCompile 'org.testng:testng:6.8.5' - testCompile 'com.google.mockwebserver:mockwebserver:20130706' - } -} - -project(':feign-slf4j') { - apply plugin: 'java' - - test { - useTestNG() - } - - dependencies { - compile project(':feign-core') - compile 'org.slf4j:slf4j-api:1.7.5' - testCompile 'org.testng:testng:6.8.5' - testCompile 'org.slf4j:slf4j-simple:1.7.5' + repositories { + jcenter() } + apply from: rootProject.file('dagger.gradle') + group = "com.netflix.${githubProjectName}" // TEMPLATE: Set to organization of project } diff --git a/codequality/HEADER b/codequality/HEADER deleted file mode 100644 index 3102e4b449..0000000000 --- a/codequality/HEADER +++ /dev/null @@ -1,13 +0,0 @@ -Copyright ${year} 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. diff --git a/core/build.gradle b/core/build.gradle new file mode 100644 index 0000000000..2600e5fdad --- /dev/null +++ b/core/build.gradle @@ -0,0 +1,15 @@ +apply plugin: 'java' + +sourceCompatibility = 1.6 + +test { + useTestNG() +} + +dependencies { + 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.5' + testCompile 'com.google.mockwebserver:mockwebserver:20130706' +} diff --git a/dagger.gradle b/dagger.gradle index 840a216165..3217a6e3e0 100644 --- a/dagger.gradle +++ b/dagger.gradle @@ -92,7 +92,7 @@ rootProject.idea.project.ipr.withXml { projectXml -> tasks.ideaModule.dependsOn(prepareAnnotationGeneratedSourceDirs) idea.module { - scopes.PROVIDED.plus += project.configurations.daggerCompiler + 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]) @@ -103,7 +103,7 @@ idea.module { tasks.eclipseClasspath.dependsOn(prepareAnnotationGeneratedSourceDirs) eclipse.classpath { - plusConfigurations += project.configurations.daggerCompiler + plusConfigurations += [project.configurations.daggerCompiler] } tasks.eclipseClasspath { diff --git a/example-github/build.gradle b/example-github/build.gradle index 631015a93b..0ecc2871d7 100644 --- a/example-github/build.gradle +++ b/example-github/build.gradle @@ -1,5 +1,11 @@ +plugins { + id 'nebula.provided-base' version '2.0.1' +} + apply plugin: 'java' +sourceCompatibility = 1.6 + dependencies { compile 'com.netflix.feign:feign-core:5.3.0' compile 'com.netflix.feign:feign-gson:5.3.0' diff --git a/example-wikipedia/build.gradle b/example-wikipedia/build.gradle index 0589c055d8..05b31b48f0 100644 --- a/example-wikipedia/build.gradle +++ b/example-wikipedia/build.gradle @@ -1,5 +1,11 @@ +plugins { + id 'nebula.provided-base' version '2.0.1' +} + apply plugin: 'java' +sourceCompatibility = 1.6 + dependencies { compile 'com.netflix.feign:feign-core:5.3.0' compile 'com.netflix.feign:feign-gson:5.3.0' diff --git a/gradle.properties b/gradle.properties index 868bb9b9a2..e69de29bb2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +0,0 @@ -version=7.0.0-SNAPSHOT diff --git a/gradle/buildscript.gradle b/gradle/buildscript.gradle deleted file mode 100644 index 0b6da7ce84..0000000000 --- a/gradle/buildscript.gradle +++ /dev/null @@ -1,11 +0,0 @@ -// Executed in context of buildscript -repositories { - // Repo in addition to maven central - repositories { maven { url 'http://dl.bintray.com/content/netflixoss/external-gradle-plugins/' } } // For gradle-release -} -dependencies { - classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.6.1' - classpath 'com.mapvine:gradle-cobertura-plugin:0.1' - classpath 'gradle-release:gradle-release:1.1.5' - classpath 'org.ajoberstar:gradle-git:0.5.0' -} diff --git a/gradle/check.gradle b/gradle/check.gradle deleted file mode 100644 index a3e4b4e7f5..0000000000 --- a/gradle/check.gradle +++ /dev/null @@ -1,26 +0,0 @@ -subprojects { -// Checkstyle -apply plugin: 'checkstyle' -checkstyle { - ignoreFailures = true - configFile = rootProject.file('codequality/checkstyle.xml') -} - -// FindBugs -apply plugin: 'findbugs' -findbugs { - ignoreFailures = true -} - -// PMD -apply plugin: 'pmd' -//tasks.withType(Pmd) { reports.html.enabled true } - -apply plugin: 'cobertura' -cobertura { - sourceDirs = sourceSets.main.java.srcDirs - format = 'html' - includes = ['**/*.java', '**/*.groovy'] - excludes = [] -} -} diff --git a/gradle/convention.gradle b/gradle/convention.gradle deleted file mode 100644 index c4658fc33e..0000000000 --- a/gradle/convention.gradle +++ /dev/null @@ -1,101 +0,0 @@ -// GRADLE-2087 workaround, perform after java plugin -status = project.hasProperty('preferredStatus')?project.preferredStatus:(version.contains('SNAPSHOT')?'snapshot':'release') - -subprojects { project -> - apply plugin: 'java' // Plugin as major conventions - - sourceCompatibility = 1.6 - - // Restore status after Java plugin - status = rootProject.status - - task sourcesJar(type: Jar, dependsOn:classes) { - from sourceSets.main.allSource - classifier 'sources' - extension 'jar' - } - - task javadocJar(type: Jar, dependsOn:javadoc) { - from javadoc.destinationDir - classifier 'javadoc' - extension 'jar' - } - - configurations.add('sources') - configurations.add('javadoc') - configurations.archives { - extendsFrom configurations.sources - extendsFrom configurations.javadoc - } - - // When outputing to an Ivy repo, we want to use the proper type field - gradle.taskGraph.whenReady { - def isNotMaven = !it.hasTask(project.uploadMavenCentral) - if (isNotMaven) { - def artifacts = project.configurations.sources.artifacts - def sourceArtifact = artifacts.iterator().next() - sourceArtifact.type = 'sources' - } - } - - artifacts { - sources(sourcesJar) { - // Weird Gradle quirk where type will be used for the extension, but only for sources - type 'jar' - } - javadoc(javadocJar) { - type 'javadoc' - } - } - - configurations { - provided { - description = 'much like compile, but indicates you expect the JDK or a container to provide it. It is only available on the compilation classpath, and is not transitive.' - transitive = true - visible = true - } - } - - project.sourceSets { - main.compileClasspath += project.configurations.provided - main.runtimeClasspath -= project.configurations.provided - test.compileClasspath += project.configurations.provided - test.runtimeClasspath += project.configurations.provided - } -} - -apply plugin: 'github-pages' // Used to create publishGhPages task - -def docTasks = [:] -[Javadoc,ScalaDoc,Groovydoc].each{ Class docClass -> - def allSources = allprojects.tasks*.withType(docClass).flatten()*.source - if (allSources) { - def shortName = docClass.simpleName.toLowerCase() - def docTask = task "aggregate${shortName.capitalize()}"(type: docClass, description: "Aggregate subproject ${shortName}s") { - source = allSources - destinationDir = file("${project.buildDir}/docs/${shortName}") - doFirst { - def classpaths = allprojects.findAll { it.plugins.hasPlugin(JavaPlugin) }.collect { it.sourceSets.main.compileClasspath } - classpath = files(classpaths) - } - } - docTasks[shortName] = docTask - processGhPages.dependsOn(docTask) - } -} - -githubPages { - repoUri = "git@github.com:Netflix/${rootProject.githubProjectName}.git" - pages { - docTasks.each { shortName, docTask -> - from(docTask.outputs.files) { - into "docs/${shortName}" - } - } - } -} - -// Generate wrapper, which is distributed as part of source to alleviate the need of installing gradle -task createWrapper(type: Wrapper) { - gradleVersion = '1.5' -} diff --git a/gradle/license.gradle b/gradle/license.gradle deleted file mode 100644 index abd2e2c0e1..0000000000 --- a/gradle/license.gradle +++ /dev/null @@ -1,10 +0,0 @@ -// Dependency for plugin was set in buildscript.gradle - -subprojects { -apply plugin: 'license' //nl.javadude.gradle.plugins.license.LicensePlugin -license { - header rootProject.file('codequality/HEADER') - ext.year = Calendar.getInstance().get(Calendar.YEAR) - skipExistingHeaders true -} -} diff --git a/gradle/maven.gradle b/gradle/maven.gradle deleted file mode 100644 index 817846d77f..0000000000 --- a/gradle/maven.gradle +++ /dev/null @@ -1,70 +0,0 @@ -// Maven side of things -subprojects { - apply plugin: 'maven' // Java plugin has to have been already applied for the conf2scope mappings to work - apply plugin: 'signing' - - signing { - required { gradle.taskGraph.hasTask(uploadMavenCentral) } - sign configurations.archives - } - -/** - * Publishing to Maven Central example provided from http://jedicoder.blogspot.com/2011/11/automated-gradle-project-deployment-to.html - * artifactory will execute uploadArchives to force generation of ivy.xml, and we don't want that to trigger an upload to maven - * central, so using custom upload task. - */ -task uploadMavenCentral(type:Upload, dependsOn: signArchives) { - configuration = configurations.archives - onlyIf { ['release', 'snapshot'].contains(project.status) } - repositories.mavenDeployer { - beforeDeployment { signing.signPom(it) } - - // To test deployment locally, use the following instead of oss.sonatype.org - //repository(url: "file://localhost/${rootProject.rootDir}/repo") - - def sonatypeUsername = rootProject.hasProperty('sonatypeUsername')?rootProject.sonatypeUsername:'' - def sonatypePassword = rootProject.hasProperty('sonatypePassword')?rootProject.sonatypePassword:'' - - repository(url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2') { - authentication(userName: sonatypeUsername, password: sonatypePassword) - } - - snapshotRepository(url: 'https://oss.sonatype.org/content/repositories/snapshots/') { - authentication(userName: sonatypeUsername, password: sonatypePassword) - } - - // Prevent datastamp from being appending to artifacts during deployment - uniqueVersion = false - - // Closure to configure all the POM with extra info, common to all projects - pom.project { - name "${project.name}" - description "${project.name} developed by Netflix" - developers { - developer { - id 'netflixgithub' - name 'Netflix Open Source Development' - email 'talent@netflix.com' - } - } - licenses { - license { - name 'The Apache Software License, Version 2.0' - url 'http://www.apache.org/licenses/LICENSE-2.0.txt' - distribution 'repo' - } - } - url "https://github.com/Netflix/${rootProject.githubProjectName}" - scm { - connection "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" - url "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" - developerConnection "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" - } - issueManagement { - system 'github' - url "https://github.com/Netflix/${rootProject.githubProjectName}/issues" - } - } - } - } -} diff --git a/gradle/netflix-oss.gradle b/gradle/netflix-oss.gradle deleted file mode 100644 index a87bc54efe..0000000000 --- a/gradle/netflix-oss.gradle +++ /dev/null @@ -1 +0,0 @@ -apply from: 'http://artifacts.netflix.com/gradle-netflix-local/artifactory.gradle' diff --git a/gradle/release.gradle b/gradle/release.gradle deleted file mode 100644 index 7979dc3a18..0000000000 --- a/gradle/release.gradle +++ /dev/null @@ -1,61 +0,0 @@ -apply plugin: 'release' - -[ uploadIvyLocal: 'uploadLocal', uploadArtifactory: 'artifactoryPublish', buildWithArtifactory: 'build' ].each { key, value -> - // Call out to compile against internal repository - task "${key}"(type: GradleBuild) { - startParameter = project.gradle.startParameter.newInstance() - doFirst { - startParameter.projectProperties = [status: project.status, preferredStatus: project.status] - } - startParameter.addInitScript( file('gradle/netflix-oss.gradle') ) - startParameter.getExcludedTaskNames().add('check') - tasks = [ 'build', value ] - } -} - -// Marker task for following code to key in on -task releaseCandidate(dependsOn: release) -task forceCandidate { - onlyIf { gradle.taskGraph.hasTask(releaseCandidate) } - doFirst { project.status = 'candidate' } -} -task forceRelease { - onlyIf { !gradle.taskGraph.hasTask(releaseCandidate) } - doFirst { project.status = 'release' } -} -release.dependsOn([forceCandidate, forceRelease]) - -task uploadMavenCentral(dependsOn: subprojects.tasks.uploadMavenCentral) -task releaseSnapshot(dependsOn: [uploadArtifactory, uploadMavenCentral]) - -// Ensure our versions look like the project status before publishing -task verifyStatus << { - def hasSnapshot = version.contains('-SNAPSHOT') - if (project.status == 'snapshot' && !hasSnapshot) { - throw new GradleException("Version (${version}) needs -SNAPSHOT if publishing snapshot") - } -} -uploadArtifactory.dependsOn(verifyStatus) -uploadMavenCentral.dependsOn(verifyStatus) - -// Ensure upload happens before taggging, hence upload failures will leave repo in a revertable state -preTagCommit.dependsOn([uploadArtifactory, uploadMavenCentral]) - - -gradle.taskGraph.whenReady { taskGraph -> - def hasRelease = taskGraph.hasTask('commitNewVersion') - def indexOf = { return taskGraph.allTasks.indexOf(it) } - - if (hasRelease) { - assert indexOf(build) < indexOf(unSnapshotVersion), 'build target has to be after unSnapshotVersion' - assert indexOf(uploadMavenCentral) < indexOf(preTagCommit), 'preTagCommit has to be after uploadMavenCentral' - assert indexOf(uploadArtifactory) < indexOf(preTagCommit), 'preTagCommit has to be after uploadArtifactory' - } -} - -// Prevent plugin from asking for a version number interactively -ext.'gradle.release.useAutomaticVersion' = "true" - -release { - git.requireBranch = null -} diff --git a/gson/build.gradle b/gson/build.gradle new file mode 100644 index 0000000000..6e6252cbd3 --- /dev/null +++ b/gson/build.gradle @@ -0,0 +1,13 @@ +apply plugin: 'java' + +sourceCompatibility = 1.6 + +test { + useTestNG() +} + +dependencies { + compile project(':feign-core') + compile 'com.google.code.gson:gson:2.2.4' + testCompile 'org.testng:testng:6.8.5' +} diff --git a/jackson/build.gradle b/jackson/build.gradle new file mode 100644 index 0000000000..edd2e0d4d4 --- /dev/null +++ b/jackson/build.gradle @@ -0,0 +1,14 @@ +apply plugin: 'java' + +sourceCompatibility = 1.6 + +test { + useTestNG() +} + +dependencies { + compile project(':feign-core') + compile 'com.fasterxml.jackson.core:jackson-databind:2.2.2' + testCompile 'org.testng:testng:6.8.5' + testCompile 'com.google.guava:guava:14.0.1' +} diff --git a/jaxb/build.gradle b/jaxb/build.gradle new file mode 100644 index 0000000000..1053548149 --- /dev/null +++ b/jaxb/build.gradle @@ -0,0 +1,11 @@ +apply plugin: 'java' + +test { + useTestNG() +} + +dependencies { + compile project(':feign-core') + testCompile 'org.testng:testng:6.8.5' + testCompile 'com.google.guava:guava:14.0.1' +} diff --git a/jaxrs/build.gradle b/jaxrs/build.gradle new file mode 100644 index 0000000000..a3f6b1ac08 --- /dev/null +++ b/jaxrs/build.gradle @@ -0,0 +1,15 @@ +apply plugin: 'java' + +sourceCompatibility = 1.6 + +test { + useTestNG() +} + +dependencies { + compile project(':feign-core') + compile 'javax.ws.rs:jsr311-api:1.1.1' + testCompile project(':feign-gson') + testCompile 'com.google.guava:guava:14.0.1' + testCompile 'org.testng:testng:6.8.5' +} diff --git a/ribbon/build.gradle b/ribbon/build.gradle new file mode 100644 index 0000000000..a01cfe0976 --- /dev/null +++ b/ribbon/build.gradle @@ -0,0 +1,14 @@ +apply plugin: 'java' + +sourceCompatibility = 1.6 + +test { + useTestNG() +} + +dependencies { + compile project(':feign-core') + compile 'com.netflix.ribbon:ribbon-loadbalancer:2.0-RC5' + testCompile 'org.testng:testng:6.8.5' + testCompile 'com.google.mockwebserver:mockwebserver:20130706' +} diff --git a/sax/build.gradle b/sax/build.gradle new file mode 100644 index 0000000000..dbb9b9a6ab --- /dev/null +++ b/sax/build.gradle @@ -0,0 +1,13 @@ +apply plugin: 'java' + +sourceCompatibility = 1.6 + +test { + useTestNG() +} + +dependencies { + compile project(':feign-core') + testCompile 'com.google.guava:guava:14.0.1' + testCompile 'org.testng:testng:6.8.5' +} diff --git a/slf4j/build.gradle b/slf4j/build.gradle new file mode 100644 index 0000000000..7b261b02f3 --- /dev/null +++ b/slf4j/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'java' + +test { + useTestNG() +} + +dependencies { + compile project(':feign-core') + compile 'org.slf4j:slf4j-api:1.7.5' + testCompile 'org.testng:testng:6.8.5' + testCompile 'org.slf4j:slf4j-simple:1.7.5' +} From 495de18ccab6d6ef00ed255778507bb572e886e1 Mon Sep 17 00:00:00 2001 From: Jinho Shin Date: Tue, 6 Jan 2015 13:56:17 +0900 Subject: [PATCH 149/179] Update README.md Hi. In JAX-RS section, adding 'JAXRSContract' is omitted. So, I didn't understand the JAX-RS example correctly. And I got an exception when I run it. If adding 'JAXRSContract' is added, It is helpful to beginners like me. Thank you. --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 27d27175b0..6132f2019b 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,19 @@ interface GitHub { @GET @Path("/repos/{owner}/{repo}/contributors") List contributors(@PathParam("owner") String owner, @PathParam("repo") String repo); } + +public static void main(String... args) { + GitHub github = Feign.builder() + .contract(new JAXRSModule.JAXRSContract()) + .decoder(new GsonDecoder()) + .target(GitHub.class, "https://api.github.com"); + + // Fetch and print a list of the contributors to this library. + List contributors = github.contributors("netflix", "feign"); + for (Contributor contributor : contributors) { + System.out.println(contributor.login + " (" + contributor.contributions + ")"); + } +} ``` ### Ribbon [RibbonModule](https://github.com/Netflix/feign/tree/master/ribbon) overrides URL resolution of Feign's client, adding smart routing and resiliency capabilities provided by [Ribbon](https://github.com/Netflix/ribbon). From 0c08ac9d333081f11bb77a24a424e83730eba397 Mon Sep 17 00:00:00 2001 From: Jinho Shin Date: Wed, 7 Jan 2015 13:46:09 +0900 Subject: [PATCH 150/179] Update README.md I removed the unessential lines. Thanks for your advice. --- README.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/README.md b/README.md index 6132f2019b..08a03af6d0 100644 --- a/README.md +++ b/README.md @@ -148,14 +148,7 @@ interface GitHub { public static void main(String... args) { GitHub github = Feign.builder() .contract(new JAXRSModule.JAXRSContract()) - .decoder(new GsonDecoder()) .target(GitHub.class, "https://api.github.com"); - - // Fetch and print a list of the contributors to this library. - List contributors = github.contributors("netflix", "feign"); - for (Contributor contributor : contributors) { - System.out.println(contributor.login + " (" + contributor.contributions + ")"); - } } ``` ### Ribbon From e6ec0766e70ce92129e28fd1309337e6156db831 Mon Sep 17 00:00:00 2001 From: Jinho Shin Date: Wed, 7 Jan 2015 15:02:06 +0900 Subject: [PATCH 151/179] Update README.md Remove more lines for unity? consistency. Customization, Request Interceptors, are written as this form. --- README.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 08a03af6d0..050b384e0f 100644 --- a/README.md +++ b/README.md @@ -144,12 +144,8 @@ interface GitHub { @GET @Path("/repos/{owner}/{repo}/contributors") List contributors(@PathParam("owner") String owner, @PathParam("repo") String repo); } - -public static void main(String... args) { - GitHub github = Feign.builder() - .contract(new JAXRSModule.JAXRSContract()) - .target(GitHub.class, "https://api.github.com"); -} +... +GitHub github = Feign.builder().contract(new JAXRSModule.JAXRSContract()).target(GitHub.class, "https://api.github.com"); ``` ### Ribbon [RibbonModule](https://github.com/Netflix/feign/tree/master/ribbon) overrides URL resolution of Feign's client, adding smart routing and resiliency capabilities provided by [Ribbon](https://github.com/Netflix/ribbon). From 73902e61f2ab7ace05950f6ac0427f5adb0830e2 Mon Sep 17 00:00:00 2001 From: Jinho Shin Date: Thu, 8 Jan 2015 15:54:46 +0900 Subject: [PATCH 152/179] Update README.md In JAX-RS example, two Java code blocks and retaining new lines after the call to each builder. --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 050b384e0f..19e501064f 100644 --- a/README.md +++ b/README.md @@ -144,8 +144,11 @@ interface GitHub { @GET @Path("/repos/{owner}/{repo}/contributors") List contributors(@PathParam("owner") String owner, @PathParam("repo") String repo); } -... -GitHub github = Feign.builder().contract(new JAXRSModule.JAXRSContract()).target(GitHub.class, "https://api.github.com"); +``` +```java +GitHub github = Feign.builder() + .contract(new JAXRSModule.JAXRSContract()) + .target(GitHub.class, "https://api.github.com"); ``` ### Ribbon [RibbonModule](https://github.com/Netflix/feign/tree/master/ribbon) overrides URL resolution of Feign's client, adding smart routing and resiliency capabilities provided by [Ribbon](https://github.com/Netflix/ribbon). From 96fa7794ec7daf16c32900b4f1517b403899584c Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Sat, 24 Jan 2015 18:49:28 +0800 Subject: [PATCH 153/179] Adds toString to Targets. Normalizes equals/hashCode. --- core/src/main/java/feign/ReflectiveFeign.java | 23 ++++++----- core/src/main/java/feign/Target.java | 32 +++++++++------ core/src/test/java/feign/FeignTest.java | 41 ++++++++++++++----- .../feign/ribbon/LoadBalancingTarget.java | 27 ++++++------ 4 files changed, 78 insertions(+), 45 deletions(-) diff --git a/core/src/main/java/feign/ReflectiveFeign.java b/core/src/main/java/feign/ReflectiveFeign.java index 5d8fe06841..6a39d6d8bb 100644 --- a/core/src/main/java/feign/ReflectiveFeign.java +++ b/core/src/main/java/feign/ReflectiveFeign.java @@ -83,27 +83,28 @@ static class FeignInvocationHandler implements InvocationHandler { } catch (IllegalArgumentException e) { return false; } - } - if ("hashCode".equals(method.getName())) { + } else if ("hashCode".equals(method.getName())) { return hashCode(); + } else if ("toString".equals(method.getName())) { + return toString(); } return dispatch.get(method).invoke(args); } - @Override public int hashCode() { - return target.hashCode(); - } - - @Override public boolean equals(Object other) { - if (other instanceof FeignInvocationHandler) { - FeignInvocationHandler that = (FeignInvocationHandler) other; - return this.target.equals(that.target); + @Override public boolean equals(Object obj) { + if (obj instanceof FeignInvocationHandler) { + FeignInvocationHandler other = (FeignInvocationHandler) obj; + return target.equals(other.target); } return false; } + @Override public int hashCode() { + return target.hashCode(); + } + @Override public String toString() { - return "target(" + target + ")"; + return target.toString(); } } diff --git a/core/src/main/java/feign/Target.java b/core/src/main/java/feign/Target.java index 894855d472..474ed29722 100644 --- a/core/src/main/java/feign/Target.java +++ b/core/src/main/java/feign/Target.java @@ -15,8 +15,6 @@ */ package feign; -import java.util.Arrays; - import static feign.Util.checkNotNull; import static feign.Util.emptyToNull; @@ -95,19 +93,29 @@ public HardCodedTarget(Class type, String name, String url) { return input.request(); } + @Override public boolean equals(Object obj) { + if (obj instanceof HardCodedTarget) { + HardCodedTarget other = (HardCodedTarget) obj; + return type.equals(other.type) + && name.equals(other.name) + && url.equals(other.url); + } + return false; + } + @Override public int hashCode() { - return Arrays.hashCode(new Object[]{type, name, url}); + int result = 17; + result = 31 * result + type.hashCode(); + result = 31 * result + name.hashCode(); + result = 31 * result + url.hashCode(); + return result; } - @Override public boolean equals(Object obj) { - if (obj == null) - return false; - if (this == obj) - return true; - if (HardCodedTarget.class != obj.getClass()) - return false; - HardCodedTarget that = HardCodedTarget.class.cast(obj); - return this.type.equals(that.type) && this.name.equals(that.name) && this.url.equals(that.url); + @Override public String toString() { + if (name.equals(url)) { + return "HardCodedTarget(type=" + type.getSimpleName() + ", url=" + url + ")"; + } + return "HardCodedTarget(type=" + type.getSimpleName() + ", name=" + name + ", url=" + url + ")"; } } } diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index 801422e255..ac64568f18 100644 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -24,6 +24,7 @@ import com.google.mockwebserver.SocketPolicy; import dagger.Module; import dagger.Provides; +import feign.Target.HardCodedTarget; import feign.codec.Decoder; import feign.codec.Encoder; import feign.codec.ErrorDecoder; @@ -46,6 +47,7 @@ import static feign.Util.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotEquals; import static org.testng.Assert.assertNull; import static org.testng.Assert.assertTrue; @@ -489,19 +491,38 @@ static class DisableHostnameVerification { } } - @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", new TestInterface.Module()); - - assertTrue(i1.equals(i1)); - assertTrue(i1.equals(i2)); - assertFalse(i1.equals(i3)); - assertFalse(i1.equals(i4)); + @Test public void equalsHashCodeAndToStringWork() { + Target t1 = new HardCodedTarget(TestInterface.class, "http://localhost:8080"); + Target t2 = new HardCodedTarget(TestInterface.class, "http://localhost:8888"); + Target t3 = + new HardCodedTarget(OtherTestInterface.class, "http://localhost:8080"); + TestInterface i1 = Feign.builder().target(t1); + TestInterface i2 = Feign.builder().target(t1); + TestInterface i3 = Feign.builder().target(t2); + OtherTestInterface i4 = Feign.builder().target(t3); + + assertEquals(i1, i1); + assertEquals(i1, i2); + assertNotEquals(i1, i3); + assertNotEquals(i1, i4); assertEquals(i1.hashCode(), i1.hashCode()); assertEquals(i1.hashCode(), i2.hashCode()); + assertNotEquals(i1.hashCode(), i3.hashCode()); + assertNotEquals(i1.hashCode(), i4.hashCode()); + + assertEquals(i1.hashCode(), t1.hashCode()); + assertEquals(i3.hashCode(), t2.hashCode()); + assertEquals(i4.hashCode(), t3.hashCode()); + + assertEquals(i1.toString(), i1.toString()); + assertEquals(i1.toString(), i2.toString()); + assertNotEquals(i1.toString(), i3.toString()); + assertNotEquals(i1.toString(), i4.toString()); + + assertEquals(i1.toString(), t1.toString()); + assertEquals(i3.toString(), t2.toString()); + assertEquals(i4.toString(), t3.toString()); } @Test public void decodeLogicSupportsByteArray() throws Exception { diff --git a/ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java b/ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java index 0894ed4817..efa18e9243 100644 --- a/ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java +++ b/ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java @@ -15,7 +15,6 @@ */ package feign.ribbon; -import com.google.common.base.Objects; import com.netflix.loadbalancer.AbstractLoadBalancer; import com.netflix.loadbalancer.Server; @@ -25,7 +24,6 @@ import feign.RequestTemplate; import feign.Target; -import static com.google.common.base.Objects.equal; import static com.netflix.client.ClientFactory.getNamedLoadBalancer; import static feign.Util.checkNotNull; import static java.lang.String.format; @@ -99,18 +97,23 @@ public AbstractLoadBalancer lb() { } } + @Override public boolean equals(Object obj) { + if (obj instanceof LoadBalancingTarget) { + LoadBalancingTarget other = (LoadBalancingTarget) obj; + return type.equals(other.type) + && name.equals(other.name); + } + return false; + } + @Override public int hashCode() { - return Objects.hashCode(type, name); + int result = 17; + result = 31 * result + type.hashCode(); + result = 31 * result + name.hashCode(); + return result; } - @Override public boolean equals(Object obj) { - if (obj == null) - return false; - if (this == obj) - return true; - if (LoadBalancingTarget.class != obj.getClass()) - return false; - LoadBalancingTarget that = LoadBalancingTarget.class.cast(obj); - return equal(this.type, that.type) && equal(this.name, that.name); + @Override public String toString() { + return "LoadBalancingTarget(type=" + type.getSimpleName() + ", name=" + name + ")"; } } From 0f3947a5c84bda1ac3bfaf5108a01f559d9b7c57 Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Sat, 24 Jan 2015 16:18:16 -0800 Subject: [PATCH 154/179] Replace TestNG with JUnit + Rules JUnit Rules, such as MockWebServerRule, reduce boilerplate setup present in our tests. By migrating off TestNG, and onto rules, our tests become more maintainable as JUnit is well understood. --- core/build.gradle | 8 +- .../test/java/feign/DefaultContractTest.java | 131 ++--- .../test/java/feign/DefaultRetryerTest.java | 39 +- .../src/test/java/feign/FeignBuilderTest.java | 103 ++-- core/src/test/java/feign/FeignTest.java | 316 +++++------- core/src/test/java/feign/LoggerTest.java | 456 ++++++++---------- .../test/java/feign/RequestTemplateTest.java | 83 ++-- core/src/test/java/feign/UtilTest.java | 18 +- .../auth/BasicAuthRequestInterceptorTest.java | 18 +- .../java/feign/codec/DefaultDecoderTest.java | 28 +- .../java/feign/codec/DefaultEncoderTest.java | 22 +- .../feign/codec/DefaultErrorDecoderTest.java | 30 +- .../feign/codec/RetryAfterDecoderTest.java | 16 +- gson/build.gradle | 6 +- .../test/java/feign/gson/GsonModuleTest.java | 28 +- jackson/build.gradle | 6 +- .../java/feign/jackson/JacksonModuleTest.java | 40 +- jaxb/build.gradle | 6 +- .../feign/jaxb/JAXBContextFactoryTest.java | 16 +- .../test/java/feign/jaxb/JAXBModuleTest.java | 31 +- jaxrs/build.gradle | 6 +- .../java/feign/jaxrs/JAXRSContractTest.java | 200 ++++---- ribbon/build.gradle | 8 +- .../feign/ribbon/LoadBalancingTargetTest.java | 33 +- .../java/feign/ribbon/RibbonClientTest.java | 68 +-- sax/build.gradle | 6 +- .../test/java/feign/sax/SAXDecoderTest.java | 33 +- slf4j/build.gradle | 6 +- .../feign/slf4j/RecordingSimpleLogger.java | 78 +++ .../test/java/feign/slf4j/ReflectionUtil.java | 37 -- .../java/feign/slf4j/SimpleLoggerUtil.java | 47 -- .../java/feign/slf4j/Slf4jLoggerTest.java | 65 +-- 32 files changed, 908 insertions(+), 1080 deletions(-) create mode 100644 slf4j/src/test/java/feign/slf4j/RecordingSimpleLogger.java delete mode 100644 slf4j/src/test/java/feign/slf4j/ReflectionUtil.java delete mode 100644 slf4j/src/test/java/feign/slf4j/SimpleLoggerUtil.java diff --git a/core/build.gradle b/core/build.gradle index 2600e5fdad..e2ee72b06c 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -2,14 +2,10 @@ apply plugin: 'java' sourceCompatibility = 1.6 -test { - useTestNG() -} - dependencies { 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.5' - testCompile 'com.google.mockwebserver:mockwebserver:20130706' + testCompile 'junit:junit:4.12' + testCompile 'com.squareup.okhttp:mockwebserver:2.2.0' } diff --git a/core/src/test/java/feign/DefaultContractTest.java b/core/src/test/java/feign/DefaultContractTest.java index e268fb7f77..77174c2517 100644 --- a/core/src/test/java/feign/DefaultContractTest.java +++ b/core/src/test/java/feign/DefaultContractTest.java @@ -16,28 +16,29 @@ package feign; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; import com.google.gson.reflect.TypeToken; -import org.testng.annotations.Test; - -import javax.inject.Named; import java.net.URI; +import java.util.Arrays; import java.util.List; - -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 javax.inject.Named; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; import static feign.Util.UTF_8; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; /** * Tests interfaces defined per {@link Contract.Default} are interpreted into expected {@link feign * .RequestTemplate template} * instances. */ -@Test public class DefaultContractTest { + @Rule public final ExpectedException thrown = ExpectedException.none(); + Contract.Default contract = new Contract.Default(); interface Methods { @@ -51,14 +52,14 @@ interface Methods { } @Test public void httpMethods() throws Exception { - assertEquals(contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("post")).template().method(), - "POST"); - assertEquals(contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("put")).template().method(), - "PUT"); - assertEquals(contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("get")).template().method(), - "GET"); - assertEquals(contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("delete")).template().method(), - "DELETE"); + assertEquals("POST", + contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("post")).template().method()); + assertEquals("PUT", + contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("put")).template().method()); + assertEquals("GET", + contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("get")).template().method()); + assertEquals("DELETE", + contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("delete")).template().method()); } interface BodyParams { @@ -78,9 +79,11 @@ interface BodyParams { }.getType()); } - @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "Method has too many Body.*") - public void tooManyBodies() throws Exception { - contract.parseAndValidatateMetadata(BodyParams.class.getDeclaredMethod("tooMany", List.class, List.class)); + @Test public void tooManyBodies() throws Exception { + thrown.expect(IllegalStateException.class); + thrown.expectMessage("Method has too many Body"); + contract.parseAndValidatateMetadata( + BodyParams.class.getDeclaredMethod("tooMany", List.class, List.class)); } interface CustomMethodAndURIParam { @@ -90,13 +93,13 @@ interface CustomMethodAndURIParam { @Test public void requestLineOnlyRequiresMethod() throws Exception { MethodMetadata md = contract.parseAndValidatateMetadata(CustomMethodAndURIParam.class.getDeclaredMethod("patch", URI.class)); - assertEquals(md.template().method(), "PATCH"); - assertEquals(md.template().url(), ""); + assertEquals("PATCH", md.template().method()); + assertEquals("", md.template().url()); assertTrue(md.template().queries().isEmpty()); assertTrue(md.template().headers().isEmpty()); assertNull(md.template().body()); assertNull(md.template().bodyTemplate()); - assertEquals(md.urlIndex(), Integer.valueOf(0)); + assertEquals(Integer.valueOf(0), md.urlIndex()); } interface WithQueryParamsInPath { @@ -114,38 +117,38 @@ interface WithQueryParamsInPath { @Test public void queryParamsInPathExtract() throws Exception { { MethodMetadata md = contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("none")); - assertEquals(md.template().url(), "/"); + assertEquals("/", md.template().url()); assertTrue(md.template().queries().isEmpty()); - assertEquals(md.template().toString(), "GET / HTTP/1.1\n"); + assertEquals("GET / HTTP/1.1\n", md.template().toString()); } { MethodMetadata md = contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("one")); - assertEquals(md.template().url(), "/"); - assertEquals(md.template().queries().get("Action"), ImmutableSet.of("GetUser")); - assertEquals(md.template().toString(), "GET /?Action=GetUser HTTP/1.1\n"); + assertEquals("/", md.template().url()); + assertEquals(Arrays.asList("GetUser"), md.template().queries().get("Action")); + assertEquals("GET /?Action=GetUser HTTP/1.1\n", md.template().toString()); } { MethodMetadata md = contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("two")); - assertEquals(md.template().url(), "/"); - assertEquals(md.template().queries().get("Action"), ImmutableSet.of("GetUser")); - assertEquals(md.template().queries().get("Version"), ImmutableSet.of("2010-05-08")); - assertEquals(md.template().toString(), "GET /?Action=GetUser&Version=2010-05-08 HTTP/1.1\n"); + assertEquals("/", md.template().url()); + assertEquals(Arrays.asList("GetUser"), md.template().queries().get("Action")); + assertEquals(Arrays.asList("2010-05-08"), md.template().queries().get("Version")); + assertEquals("GET /?Action=GetUser&Version=2010-05-08 HTTP/1.1\n", md.template().toString()); } { MethodMetadata md = contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("three")); - assertEquals(md.template().url(), "/"); - assertEquals(md.template().queries().get("Action"), ImmutableSet.of("GetUser")); - assertEquals(md.template().queries().get("Version"), ImmutableSet.of("2010-05-08")); - assertEquals(md.template().queries().get("limit"), ImmutableSet.of("1")); - assertEquals(md.template().toString(), "GET /?Action=GetUser&Version=2010-05-08&limit=1 HTTP/1.1\n"); + assertEquals("/", md.template().url()); + assertEquals(Arrays.asList("GetUser"), md.template().queries().get("Action")); + assertEquals(Arrays.asList("2010-05-08"), md.template().queries().get("Version")); + assertEquals(Arrays.asList("1"), md.template().queries().get("limit")); + assertEquals("GET /?Action=GetUser&Version=2010-05-08&limit=1 HTTP/1.1\n", md.template().toString()); } { MethodMetadata md = contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("empty")); - assertEquals(md.template().url(), "/"); + assertEquals("/", md.template().url()); assertTrue(md.template().queries().containsKey("flag")); - assertEquals(md.template().queries().get("Action"), ImmutableSet.of("GetUser")); - assertEquals(md.template().queries().get("Version"), ImmutableSet.of("2010-05-08")); - assertEquals(md.template().toString(), "GET /?flag&Action=GetUser&Version=2010-05-08 HTTP/1.1\n"); + assertEquals(Arrays.asList("GetUser"), md.template().queries().get("Action")); + assertEquals(Arrays.asList("2010-05-08"), md.template().queries().get("Version")); + assertEquals("GET /?flag&Action=GetUser&Version=2010-05-08 HTTP/1.1\n", md.template().toString()); } } @@ -156,9 +159,8 @@ interface BodyWithoutParameters { } @Test public void bodyWithoutParameters() throws Exception { - String expectedBody = ""; MethodMetadata md = contract.parseAndValidatateMetadata(BodyWithoutParameters.class.getDeclaredMethod("post")); - assertEquals(md.template().body(), expectedBody.getBytes(UTF_8)); + assertEquals("", new String(md.template().body(), UTF_8)); assertFalse(md.template().bodyTemplate() != null); assertTrue(md.formParams().isEmpty()); assertTrue(md.indexToName().isEmpty()); @@ -166,7 +168,7 @@ interface BodyWithoutParameters { @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")); + assertEquals(Arrays.asList("application/xml"), md.template().headers().get("Content-Type")); } interface WithURIParam { @@ -176,15 +178,15 @@ interface WithURIParam { @Test public void methodCanHaveUriParam() throws Exception { MethodMetadata md = contract.parseAndValidatateMetadata(WithURIParam.class.getDeclaredMethod("uriParam", String.class, URI.class, String.class)); - assertEquals(md.urlIndex(), Integer.valueOf(1)); + assertEquals(Integer.valueOf(1), md.urlIndex()); } @Test public void pathParamsParseIntoIndexToName() throws Exception { MethodMetadata md = contract.parseAndValidatateMetadata(WithURIParam.class.getDeclaredMethod("uriParam", String.class, URI.class, String.class)); - assertEquals(md.template().url(), "/{1}/{2}"); - assertEquals(md.indexToName().get(0), ImmutableSet.of("1")); - assertEquals(md.indexToName().get(2), ImmutableSet.of("2")); + assertEquals("/{1}/{2}", md.template().url()); + assertEquals(Arrays.asList("1"), md.indexToName().get(0)); + assertEquals(Arrays.asList("2"), md.indexToName().get(2)); } interface WithPathAndQueryParams { @@ -199,13 +201,13 @@ Response recordsByNameAndType(@Named("domainId") int id, @Named("name") String n assertNull(md.template().body()); assertNull(md.template().bodyTemplate()); assertTrue(md.template().headers().isEmpty()); - assertEquals(md.template().url(), "/domains/{domainId}/records"); - assertEquals(md.template().queries().get("name"), ImmutableSet.of("{name}")); - assertEquals(md.template().queries().get("type"), ImmutableSet.of("{type}")); - assertEquals(md.indexToName().get(0), ImmutableSet.of("domainId")); - assertEquals(md.indexToName().get(1), ImmutableSet.of("name")); - assertEquals(md.indexToName().get(2), ImmutableSet.of("type")); - assertEquals(md.template().toString(), "GET /domains/{domainId}/records?name={name}&type={type} HTTP/1.1\n"); + assertEquals("/domains/{domainId}/records", md.template().url()); + assertEquals(Arrays.asList("{name}"), md.template().queries().get("name")); + assertEquals(Arrays.asList("{type}"), md.template().queries().get("type")); + assertEquals(Arrays.asList("domainId"), md.indexToName().get(0)); + assertEquals(Arrays.asList("name"), md.indexToName().get(1)); + assertEquals(Arrays.asList("type"), md.indexToName().get(2)); + assertEquals("GET /domains/{domainId}/records?name={name}&type={type} HTTP/1.1\n", md.template().toString()); } interface FormParams { @@ -221,12 +223,13 @@ void login( 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"); - 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")); + assertEquals( + "%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D", + md.template().bodyTemplate()); + assertEquals(ImmutableList.of("customer_name", "user_name", "password"), md.formParams()); + assertEquals(Arrays.asList("customer_name"), md.indexToName().get(0)); + assertEquals(Arrays.asList("user_name"), md.indexToName().get(1)); + assertEquals(Arrays.asList("password"), md.indexToName().get(2)); } interface HeaderParams { @@ -237,7 +240,7 @@ interface HeaderParams { @Test public void headerParamsParseIntoIndexToName() throws Exception { MethodMetadata md = contract.parseAndValidatateMetadata(HeaderParams.class.getDeclaredMethod("logout", String.class)); - assertEquals(md.template().headers().get("Auth-Token"), ImmutableSet.of("{Auth-Token}")); - assertEquals(md.indexToName().get(0), ImmutableSet.of("Auth-Token")); + assertEquals(Arrays.asList("{Auth-Token}"), md.template().headers().get("Auth-Token")); + assertEquals(Arrays.asList("Auth-Token"), md.indexToName().get(0)); } } diff --git a/core/src/test/java/feign/DefaultRetryerTest.java b/core/src/test/java/feign/DefaultRetryerTest.java index 6ccc9c6857..a73cdbed4f 100644 --- a/core/src/test/java/feign/DefaultRetryerTest.java +++ b/core/src/test/java/feign/DefaultRetryerTest.java @@ -15,42 +15,41 @@ */ package feign; -import org.testng.annotations.Test; - +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; import java.util.Date; - import feign.Retryer.Default; -import static org.testng.Assert.assertEquals; +import static org.junit.Assert.assertEquals; -@Test public class DefaultRetryerTest { + @Rule public final ExpectedException thrown = ExpectedException.none(); - @Test(expectedExceptions = RetryableException.class) - public void only5TriesAllowedAndExponentialBackoff() throws Exception { + @Test public void only5TriesAllowedAndExponentialBackoff() throws Exception { RetryableException e = new RetryableException(null, null, null); Default retryer = new Retryer.Default(); - assertEquals(retryer.attempt, 1); - assertEquals(retryer.sleptForMillis, 0); + assertEquals(1, retryer.attempt); + assertEquals(0, retryer.sleptForMillis); retryer.continueOrPropagate(e); - assertEquals(retryer.attempt, 2); - assertEquals(retryer.sleptForMillis, 150); + assertEquals(2, retryer.attempt); + assertEquals(150, retryer.sleptForMillis); retryer.continueOrPropagate(e); - assertEquals(retryer.attempt, 3); - assertEquals(retryer.sleptForMillis, 375); + assertEquals(3, retryer.attempt); + assertEquals(375, retryer.sleptForMillis); retryer.continueOrPropagate(e); - assertEquals(retryer.attempt, 4); - assertEquals(retryer.sleptForMillis, 712); + assertEquals(4, retryer.attempt); + assertEquals(712, retryer.sleptForMillis); retryer.continueOrPropagate(e); - assertEquals(retryer.attempt, 5); - assertEquals(retryer.sleptForMillis, 1218); + assertEquals(5, retryer.attempt); + assertEquals(1218, retryer.sleptForMillis); + thrown.expect(RetryableException.class); retryer.continueOrPropagate(e); - // fail } @Test public void considersRetryAfterButNotMoreThanMaxPeriod() throws Exception { @@ -61,7 +60,7 @@ protected long currentTimeMillis() { }; retryer.continueOrPropagate(new RetryableException(null, null, new Date(5000))); - assertEquals(retryer.attempt, 2); - assertEquals(retryer.sleptForMillis, 1000); + assertEquals(2, retryer.attempt); + assertEquals(1000, retryer.sleptForMillis); } } diff --git a/core/src/test/java/feign/FeignBuilderTest.java b/core/src/test/java/feign/FeignBuilderTest.java index c9a3ef8f20..1ab2f58333 100644 --- a/core/src/test/java/feign/FeignBuilderTest.java +++ b/core/src/test/java/feign/FeignBuilderTest.java @@ -15,13 +15,13 @@ */ package feign; -import com.google.mockwebserver.MockResponse; -import com.google.mockwebserver.MockWebServer; -import com.google.mockwebserver.RecordedRequest; +import com.squareup.okhttp.mockwebserver.MockResponse; +import com.squareup.okhttp.mockwebserver.RecordedRequest; +import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule; import feign.codec.Decoder; import feign.codec.EncodeException; import feign.codec.Encoder; -import org.testng.annotations.Test; +import org.junit.Rule; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; @@ -30,10 +30,13 @@ import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Test; -import static org.testng.Assert.assertEquals; +import static org.junit.Assert.assertEquals; public class FeignBuilderTest { + @Rule public final MockWebServerRule server = new MockWebServerRule(); + interface TestInterface { @RequestLine("POST /") Response codecPost(String data); @@ -43,26 +46,20 @@ interface TestInterface { } @Test public void testDefaults() throws Exception { - MockWebServer server = new MockWebServer(); server.enqueue(new MockResponse().setBody("response data")); - server.play(); String url = "http://localhost:" + server.getPort(); - try { - TestInterface api = Feign.builder().target(TestInterface.class, url); - Response response = api.codecPost("request data"); - assertEquals(Util.toString(response.body().asReader()), "response data"); - } finally { - server.shutdown(); - assertEquals(server.getRequestCount(), 1); - assertEquals(server.takeRequest().getUtf8Body(), "request data"); - } + TestInterface api = Feign.builder().target(TestInterface.class, url); + + Response response = api.codecPost("request data"); + assertEquals("response data", Util.toString(response.body().asReader())); + + assertEquals(1, server.getRequestCount()); + assertEquals("request data", server.takeRequest().getUtf8Body()); } @Test public void testOverrideEncoder() throws Exception { - MockWebServer server = new MockWebServer(); server.enqueue(new MockResponse().setBody("response data")); - server.play(); String url = "http://localhost:" + server.getPort(); Encoder encoder = new Encoder() { @@ -71,20 +68,16 @@ public void encode(Object object, RequestTemplate template) throws EncodeExcepti template.body(object.toString()); } }; - try { - TestInterface api = Feign.builder().encoder(encoder).target(TestInterface.class, url); - api.encodedPost(Arrays.asList("This", "is", "my", "request")); - } finally { - server.shutdown(); - assertEquals(server.getRequestCount(), 1); - assertEquals(server.takeRequest().getUtf8Body(), "[This, is, my, request]"); - } + + TestInterface api = Feign.builder().encoder(encoder).target(TestInterface.class, url); + api.encodedPost(Arrays.asList("This", "is", "my", "request")); + + assertEquals(1, server.getRequestCount()); + assertEquals("[This, is, my, request]", server.takeRequest().getUtf8Body()); } @Test public void testOverrideDecoder() throws Exception { - MockWebServer server = new MockWebServer(); server.enqueue(new MockResponse().setBody("success!")); - server.play(); String url = "http://localhost:" + server.getPort(); Decoder decoder = new Decoder() { @@ -94,19 +87,14 @@ public Object decode(Response response, Type type) { } }; - try { - TestInterface api = Feign.builder().decoder(decoder).target(TestInterface.class, url); - assertEquals(api.decodedPost(), "fail"); - } finally { - server.shutdown(); - assertEquals(server.getRequestCount(), 1); - } + TestInterface api = Feign.builder().decoder(decoder).target(TestInterface.class, url); + assertEquals("fail", api.decodedPost()); + + assertEquals(1, server.getRequestCount()); } @Test public void testProvideRequestInterceptors() throws Exception { - MockWebServer server = new MockWebServer(); server.enqueue(new MockResponse().setBody("response data")); - server.play(); String url = "http://localhost:" + server.getPort(); RequestInterceptor requestInterceptor = new RequestInterceptor() { @@ -115,23 +103,19 @@ public void apply(RequestTemplate template) { template.header("Content-Type", "text/plain"); } }; - try { - TestInterface api = Feign.builder().requestInterceptor(requestInterceptor).target(TestInterface.class, url); - Response response = api.codecPost("request data"); - assertEquals(Util.toString(response.body().asReader()), "response data"); - } finally { - server.shutdown(); - assertEquals(server.getRequestCount(), 1); - RecordedRequest request = server.takeRequest(); - assertEquals(request.getUtf8Body(), "request data"); - assertEquals(request.getHeader("Content-Type"), "text/plain"); - } + + TestInterface api = Feign.builder().requestInterceptor(requestInterceptor).target(TestInterface.class, url); + Response response = api.codecPost("request data"); + assertEquals(Util.toString(response.body().asReader()), "response data"); + + assertEquals(1, server.getRequestCount()); + RecordedRequest request = server.takeRequest(); + assertEquals("request data", request.getUtf8Body()); + assertEquals("text/plain", request.getHeader("Content-Type")); } @Test public void testProvideInvocationHandlerFactory() throws Exception { - MockWebServer server = new MockWebServer(); server.enqueue(new MockResponse().setBody("response data")); - server.play(); String url = "http://localhost:" + server.getPort(); @@ -144,16 +128,13 @@ public void apply(RequestTemplate template) { } }; - try { - TestInterface api = Feign.builder().invocationHandlerFactory(factory).target(TestInterface.class, url); - Response response = api.codecPost("request data"); - assertEquals(Util.toString(response.body().asReader()), "response data"); - assertEquals(callCount.get(), 1); - } finally { - server.shutdown(); - assertEquals(server.getRequestCount(), 1); - RecordedRequest request = server.takeRequest(); - assertEquals(request.getUtf8Body(), "request data"); - } + TestInterface api = Feign.builder().invocationHandlerFactory(factory).target(TestInterface.class, url); + Response response = api.codecPost("request data"); + assertEquals("response data", Util.toString(response.body().asReader())); + assertEquals(1, callCount.get()); + + assertEquals(1, server.getRequestCount()); + RecordedRequest request = server.takeRequest(); + assertEquals("request data", request.getUtf8Body()); } } diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index ac64568f18..f63a122581 100644 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -18,10 +18,11 @@ 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 com.squareup.okhttp.mockwebserver.MockResponse; +import com.squareup.okhttp.mockwebserver.MockWebServer; +import com.squareup.okhttp.mockwebserver.RecordedRequest; +import com.squareup.okhttp.mockwebserver.SocketPolicy; +import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule; import dagger.Module; import dagger.Provides; import feign.Target.HardCodedTarget; @@ -29,12 +30,6 @@ import feign.codec.Encoder; import feign.codec.ErrorDecoder; import feign.codec.StringDecoder; -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.lang.reflect.Type; import java.net.URI; @@ -42,19 +37,27 @@ import java.util.List; import java.util.Map; import java.util.concurrent.Executor; +import javax.inject.Named; +import javax.inject.Singleton; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSocketFactory; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; 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.assertNotEquals; -import static org.testng.Assert.assertNull; -import static org.testng.Assert.assertTrue; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; -@Test // unbound wildcards are not currently injectable in dagger. @SuppressWarnings("rawtypes") public class FeignTest { + @Rule public final ExpectedException thrown = ExpectedException.none(); + @Rule public final MockWebServerRule server = new MockWebServerRule(); interface TestInterface { @RequestLine("POST /") Response response(); @@ -99,18 +102,12 @@ static class Module { @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()); + 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(); - } + api.queryParams("user", Arrays.asList("apple", "pear")); + assertEquals("GET /?1=user&2=apple&2=pear HTTP/1.1", server.takeRequest().getRequestLine()); } interface OtherTestInterface { @@ -134,93 +131,63 @@ static class RunSynchronous { @Test public void postTemplateParamsResolve() 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()); + TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module()); - api.login("netflix", "denominator", "password"); - assertEquals(new String(server.takeRequest().getBody(), UTF_8), - "{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}"); - } finally { - server.shutdown(); - } + api.login("netflix", "denominator", "password"); + assertEquals("{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}", + server.takeRequest().getUtf8Body()); } @Test public void responseCoercesToStringBody() 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()); + TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), + new TestInterface.Module()); - Response response = api.response(); - assertTrue(response.body().isRepeatable()); - assertEquals(response.body().toString(), "foo"); - } finally { - server.shutdown(); - } + Response response = api.response(); + assertTrue(response.body().isRepeatable()); + assertEquals("foo", response.body().toString()); } @Test public void postFormParams() 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()); + TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module()); - api.form("netflix", "denominator", "password"); - assertEquals(new String(server.takeRequest().getBody(), UTF_8), - "customer_name=netflix,user_name=denominator,password=password"); - } finally { - server.shutdown(); - } + api.form("netflix", "denominator", "password"); + assertEquals("customer_name=netflix,user_name=denominator,password=password", + server.takeRequest().getUtf8Body()); } @Test public void postBodyParam() 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()); + TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module()); - api.body(Arrays.asList("netflix", "denominator", "password")); - RecordedRequest request = server.takeRequest(); - assertEquals(request.getHeader("Content-Length"), "32"); - assertEquals(new String(request.getBody(), UTF_8), "[netflix, denominator, password]"); - } finally { - server.shutdown(); - } + api.body(Arrays.asList("netflix", "denominator", "password")); + RecordedRequest request = server.takeRequest(); + assertEquals("32", request.getHeader("Content-Length")); + assertEquals("[netflix, denominator, password]", request.getUtf8Body()); } @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(); - } + 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("[netflix, denominator, password]", uncompressedBody); } @Module(library = true) @@ -236,19 +203,13 @@ static class ForwardedForInterceptor implements RequestInterceptor { @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()); + 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(); - } + api.post(); + assertEquals("origin.host.com", server.takeRequest().getHeader("X-Forwarded-For")); } @Module(library = true) @@ -264,27 +225,21 @@ static class UserAgentInterceptor implements RequestInterceptor { @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()); + 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(); - } + api.post(); + RecordedRequest request = server.takeRequest(); + assertEquals("origin.host.com", request.getHeader("X-Forwarded-For")); + assertEquals("Feign", request.getHeader("User-Agent")); } @Test public void toKeyMethodFormatsAsExpected() throws Exception { - assertEquals(Feign.configKey(TestInterface.class.getDeclaredMethod("post")), "TestInterface#post()"); - assertEquals(Feign.configKey(TestInterface.class.getDeclaredMethod("uriParam", String.class, URI.class, - String.class)), "TestInterface#uriParam(String,URI,String)"); + assertEquals("TestInterface#post()", Feign.configKey(TestInterface.class.getDeclaredMethod("post"))); + assertEquals("TestInterface#uriParam(String,URI,String)", Feign.configKey( + TestInterface.class.getDeclaredMethod("uriParam", String.class, URI.class, String.class))); } @dagger.Module(overrides = true, library = true, includes = TestInterface.Module.class) @@ -303,39 +258,27 @@ public Exception decode(String methodKey, Response response) { } } - @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "zone not found") + @Test public void canOverrideErrorDecoder() throws IOException, InterruptedException { - - final MockWebServer server = new MockWebServer(); server.enqueue(new MockResponse().setResponseCode(404).setBody("foo")); - server.play(); + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("zone not found"); - try { - TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), + TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new IllegalArgumentExceptionOn404()); - api.post(); - } finally { - server.shutdown(); - } + api.post(); } @Test public void retriesLostConnectionBeforeRead() throws IOException, InterruptedException { - MockWebServer server = new MockWebServer(); server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); - server.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8))); - server.play(); - - try { - TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), - new TestInterface.Module()); + server.enqueue(new MockResponse().setBody("success!")); - api.post(); - assertEquals(server.getRequestCount(), 2); + TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), + new TestInterface.Module()); - } finally { - server.shutdown(); - } + api.post(); + assertEquals(2, server.getRequestCount()); } @dagger.Module(overrides = true, library = true, includes = TestInterface.Module.class) @@ -351,19 +294,13 @@ public Object decode(Response response, Type type) { } public void overrideTypeSpecificDecoder() throws IOException, InterruptedException { - MockWebServer server = new MockWebServer(); - server.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8))); - server.play(); + server.enqueue(new MockResponse().setBody("success!")); - try { - TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), - new DecodeFail()); + TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), + new DecodeFail()); - assertEquals(api.post(), "fail"); - } finally { - server.shutdown(); - assertEquals(server.getRequestCount(), 1); - } + assertEquals(api.post(), "fail"); + assertEquals(1, server.getRequestCount()); } @dagger.Module(overrides = true, library = true, includes = TestInterface.Module.class) @@ -385,20 +322,14 @@ public Object decode(Response response, Type type) throws IOException, FeignExce * 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().setBody("retry!".getBytes(UTF_8))); - server.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8))); - server.play(); + server.enqueue(new MockResponse().setBody("retry!")); + server.enqueue(new MockResponse().setBody("success!")); - try { - TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), - new RetryableExceptionOnRetry()); + TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), + new RetryableExceptionOnRetry()); - assertEquals(api.post(), "success!"); - } finally { - server.shutdown(); - assertEquals(server.getRequestCount(), 2); - } + assertEquals(api.post(), "success!"); + assertEquals(2, server.getRequestCount()); } @dagger.Module(overrides = true, library = true, includes = TestInterface.Module.class) @@ -413,20 +344,19 @@ public Object decode(Response response, Type type) throws IOException { } } - @Test(expectedExceptions = FeignException.class, expectedExceptionsMessageRegExp = "error reading response POST http://.*") + @Test public void doesntRetryAfterResponseIsSent() throws IOException, InterruptedException { - MockWebServer server = new MockWebServer(); - server.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8))); - server.play(); + server.enqueue(new MockResponse().setBody("success!")); + thrown.expect(FeignException.class); + thrown.expectMessage("error reading response POST http://"); - try { - TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), - new IOEOnDecode()); + TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), + new IOEOnDecode()); + try { api.post(); } finally { - server.shutdown(); - assertEquals(server.getRequestCount(), 1); + assertEquals(1, server.getRequestCount()); } } @@ -440,7 +370,7 @@ static class TrustSSLSockets { @Test public void canOverrideSSLSocketFactory() throws IOException, InterruptedException { MockWebServer server = new MockWebServer(); server.useHttps(TrustingSSLSocketFactory.get("localhost"), false); - server.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8))); + server.enqueue(new MockResponse().setBody("success!")); server.play(); try { @@ -462,7 +392,7 @@ static class DisableHostnameVerification { @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(UTF_8))); + server.enqueue(new MockResponse().setBody("success!")); server.play(); try { @@ -478,14 +408,14 @@ static class DisableHostnameVerification { MockWebServer server = new MockWebServer(); server.useHttps(TrustingSSLSocketFactory.get("localhost"), false); server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE)); - server.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8))); + server.enqueue(new MockResponse().setBody("success!")); server.play(); try { TestInterface api = Feign.create(TestInterface.class, "https://localhost:" + server.getPort(), new TestInterface.Module(), new TrustSSLSockets()); api.post(); - assertEquals(server.getRequestCount(), 2); + assertEquals(2, server.getRequestCount()); } finally { server.shutdown(); } @@ -502,59 +432,47 @@ static class DisableHostnameVerification { OtherTestInterface i4 = Feign.builder().target(t3); assertEquals(i1, i1); - assertEquals(i1, i2); - assertNotEquals(i1, i3); - assertNotEquals(i1, i4); + assertEquals(i2, i1); + assertNotEquals(i3, i1); + assertNotEquals(i4, i1); assertEquals(i1.hashCode(), i1.hashCode()); - assertEquals(i1.hashCode(), i2.hashCode()); - assertNotEquals(i1.hashCode(), i3.hashCode()); - assertNotEquals(i1.hashCode(), i4.hashCode()); + assertEquals(i2.hashCode(), i1.hashCode()); + assertNotEquals(i3.hashCode(), i1.hashCode()); + assertNotEquals(i4.hashCode(), i1.hashCode()); - assertEquals(i1.hashCode(), t1.hashCode()); - assertEquals(i3.hashCode(), t2.hashCode()); - assertEquals(i4.hashCode(), t3.hashCode()); + assertEquals(t1.hashCode(), i1.hashCode()); + assertEquals(t2.hashCode(), i3.hashCode()); + assertEquals(t3.hashCode(), i4.hashCode()); assertEquals(i1.toString(), i1.toString()); - assertEquals(i1.toString(), i2.toString()); - assertNotEquals(i1.toString(), i3.toString()); - assertNotEquals(i1.toString(), i4.toString()); + assertEquals(i2.toString(), i1.toString()); + assertNotEquals(i3.toString(), i1.toString()); + assertNotEquals(i4.toString(), i1.toString()); - assertEquals(i1.toString(), t1.toString()); - assertEquals(i3.toString(), t2.toString()); - assertEquals(i4.toString(), t3.toString()); + assertEquals(t1.toString(), i1.toString()); + assertEquals(t2.toString(), i3.toString()); + assertEquals(t3.toString(), i4.toString()); } @Test public void decodeLogicSupportsByteArray() throws Exception { byte[] expectedResponse = {12, 34, 56}; - final MockWebServer server = new MockWebServer(); server.enqueue(new MockResponse().setBody(expectedResponse)); - server.play(); - try { - OtherTestInterface api = Feign.builder().target(OtherTestInterface.class, "http://localhost:" + server.getPort()); + OtherTestInterface api = Feign.builder().target(OtherTestInterface.class, "http://localhost:" + server.getPort()); - byte[] actualResponse = api.binaryResponseBody(); - assertEquals(actualResponse, expectedResponse); - } finally { - server.shutdown(); - } + byte[] actualResponse = api.binaryResponseBody(); + assertArrayEquals(expectedResponse, actualResponse); } @Test public void encodeLogicSupportsByteArray() throws Exception { byte[] expectedRequest = {12, 34, 56}; - final MockWebServer server = new MockWebServer(); server.enqueue(new MockResponse()); - server.play(); - try { - OtherTestInterface api = Feign.builder().target(OtherTestInterface.class, "http://localhost:" + server.getPort()); + OtherTestInterface api = Feign.builder().target(OtherTestInterface.class, "http://localhost:" + server.getPort()); - api.binaryRequestBody(expectedRequest); - byte[] actualRequest = server.takeRequest().getBody(); - assertEquals(actualRequest, expectedRequest); - } finally { - server.shutdown(); - } + api.binaryRequestBody(expectedRequest); + byte[] actualRequest = server.takeRequest().getBody(); + assertArrayEquals(expectedRequest, actualRequest); } } diff --git a/core/src/test/java/feign/LoggerTest.java b/core/src/test/java/feign/LoggerTest.java index 0d32a36d11..5e2001152d 100644 --- a/core/src/test/java/feign/LoggerTest.java +++ b/core/src/test/java/feign/LoggerTest.java @@ -16,45 +16,38 @@ package feign; 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 org.testng.annotations.BeforeMethod; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; - -import javax.inject.Named; -import javax.inject.Singleton; +import com.squareup.okhttp.mockwebserver.MockResponse; +import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule; +import feign.Logger.Level; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; +import javax.inject.Named; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.rules.ExpectedException; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.junit.runners.model.Statement; import static feign.Util.UTF_8; -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertTrue; -import static org.testng.Assert.fail; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; -@Test +@RunWith(Enclosed.class) public class LoggerTest { - - Logger logger = new Logger() { - @Override protected void log(String configKey, String format, Object... args) { - messages.add(methodTag(configKey) + String.format(format, args)); - } - }; - - List messages = new ArrayList(); - - @BeforeMethod void clear() { - messages.clear(); - } + @Rule public final MockWebServerRule server = new MockWebServerRule(); + @Rule public final RecordingLogger logger = new RecordingLogger(); + @Rule public final ExpectedException thrown = ExpectedException.none(); interface SendsStuff { - @RequestLine("POST /") @Headers("Content-Type: application/json") @Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D") @@ -63,265 +56,238 @@ String login( @Named("user_name") String user, @Named("password") String password); } - @DataProvider(name = "levelToOutput") - public Object[][] levelToOutput() { - Object[][] data = new Object[4][2]; - data[0][0] = Logger.Level.NONE; - data[0][1] = Arrays.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\\)" - ); - 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\\] <--- END HTTP \\(3-byte body\\)" - ); - 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\\] foo", - "\\[SendsStuff#login\\] <--- END HTTP \\(3-byte body\\)" - ); - return data; - } + @RunWith(Parameterized.class) + public static class LogLevelEmitsTest extends LoggerTest { + private final Level logLevel; + + @Parameters + public static Iterable data() { + return Arrays.asList(new Object[][] { + { Level.NONE, Arrays.asList() }, + { Level.BASIC, Arrays.asList( + "\\[SendsStuff#login\\] ---> POST http://localhost:[0-9]+/ HTTP/1.1", + "\\[SendsStuff#login\\] <--- HTTP/1.1 200 OK \\([0-9]+ms\\)") }, + { Level.HEADERS, 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\\] <--- END HTTP \\(3-byte body\\)") }, + { Level.FULL, 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\\] foo", + "\\[SendsStuff#login\\] <--- END HTTP \\(3-byte body\\)") } + }); + } + + public LogLevelEmitsTest(Level logLevel, List expectedMessages) { + this.logLevel = logLevel; + logger.expectMessages(expectedMessages); + } - @Test(dataProvider = "levelToOutput") - public void levelEmits(final Logger.Level logLevel, List expectedMessages) throws IOException, InterruptedException { - final MockWebServer server = new MockWebServer(); - server.enqueue(new MockResponse().setBody("foo")); - server.play(); + @Test public void levelEmits() throws IOException, InterruptedException { + server.enqueue(new MockResponse().setBody("foo")); - try { - SendsStuff api = Feign.create(SendsStuff.class, "http://localhost:" + server.getUrl("").getPort(), - new DefaultModule(logger, logLevel)); + SendsStuff api = Feign.builder() + .logger(logger) + .logLevel(logLevel) + .target(SendsStuff.class, "http://localhost:" + server.getUrl("").getPort()); 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)); - } - assertEquals(new String(server.takeRequest().getBody(), UTF_8), "{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}"); - } finally { - server.shutdown(); } } - static @dagger.Module(injects = Feign.class, overrides = true, addsTo = Feign.Defaults.class) class DefaultModule { - final Logger logger; - final Logger.Level logLevel; - - DefaultModule(Logger logger, Logger.Level logLevel) { - this.logger = logger; - this.logLevel = logLevel; + @RunWith(Parameterized.class) + public static class ReadTimeoutEmitsTest extends LoggerTest { + private final Level logLevel; + + @Parameters + public static Iterable data() { + return Arrays.asList(new Object[][] { + { Level.NONE, Arrays.asList() }, + { Level.BASIC, 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\\)") }, + { Level.HEADERS, 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\\)") }, + { Level.FULL, 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") } + }); } - @Provides Decoder defaultDecoder() { - return new Decoder.Default(); + public ReadTimeoutEmitsTest(Level logLevel, List expectedMessages) { + this.logLevel = logLevel; + logger.expectMessages(expectedMessages); } - @Provides Encoder defaultEncoder() { - return new Encoder.Default(); - } + @Test public void levelEmitsOnReadTimeout() throws IOException, InterruptedException { + server.enqueue(new MockResponse().throttleBody(1, 1, TimeUnit.SECONDS).setBody("foo")); + thrown.expect(FeignException.class); - @Provides @Singleton Logger logger() { - return logger; - } + SendsStuff api = Feign.builder() + .logger(logger) + .logLevel(logLevel) + .options(new Request.Options(10 * 1000, 50)) + .target(SendsStuff.class, "http://localhost:" + server.getUrl("").getPort()); - @Provides @Singleton Logger.Level level() { - return logLevel; + api.login("netflix", "denominator", "password"); } } - @DataProvider(name = "levelToReadTimeoutOutput") - public Object[][] levelToReadTimeoutOutput() { - Object[][] data = new Object[4][2]; - data[0][0] = Logger.Level.NONE; - data[0][1] = Arrays.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; - } + @RunWith(Parameterized.class) + public static class UnknownHostEmitsTest extends LoggerTest { + private final Level logLevel; + + @Parameters + public static Iterable data() { + return Arrays.asList(new Object[][] { + { Level.NONE, Arrays.asList() }, + { Level.BASIC, Arrays.asList( + "\\[SendsStuff#login\\] ---> POST http://robofu.abc/ HTTP/1.1", + "\\[SendsStuff#login\\] <--- ERROR UnknownHostException: robofu.abc \\([0-9]+ms\\)") }, + { Level.HEADERS, 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\\)") }, + { Level.FULL, 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") } + }); + } - @dagger.Module(overrides = true, library = true) - static class LessReadTimeoutModule { - @Provides Request.Options lessReadTimeout() { - return new Request.Options(10 * 1000, 50); + public UnknownHostEmitsTest(Level logLevel, List expectedMessages) { + this.logLevel = logLevel; + logger.expectMessages(expectedMessages); } - } - @Test(dataProvider = "levelToReadTimeoutOutput") - public void readTimeoutEmits(final Logger.Level logLevel, List expectedMessages) throws IOException, InterruptedException { - final MockWebServer server = new MockWebServer(); - server.enqueue(new MockResponse().setBytesPerSecond(1).setBody("foo")); - server.play(); + @Test public void unknownHostEmits() throws IOException, InterruptedException { + SendsStuff api = Feign.builder() + .logger(logger) + .logLevel(logLevel) + .retryer(new Retryer() { + @Override public void continueOrPropagate(RetryableException e) { + throw e; + } + }) + .target(SendsStuff.class, "http://robofu.abc"); - try { - SendsStuff api = Feign.create(SendsStuff.class, "http://localhost:" + server.getUrl("").getPort(), - new LessReadTimeoutModule(), new DefaultModule(logger, logLevel)); + thrown.expect(FeignException.class); api.login("netflix", "denominator", "password"); - - fail(); - } catch (FeignException e) { - - assertMessagesMatch(expectedMessages); - - assertEquals(new String(server.takeRequest().getBody(), UTF_8), - "{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}"); - } finally { - server.shutdown(); } } - @DataProvider(name = "levelToUnknownHostOutput") - public Object[][] levelToUnknownHostOutput() { - Object[][] data = new Object[4][2]; - data[0][0] = Logger.Level.NONE; - data[0][1] = Arrays.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; - } - }; + @RunWith(Parameterized.class) + public static class RetryEmitsTest extends LoggerTest { + private final Level logLevel; + + @Parameters + public static Iterable data() { + return Arrays.asList(new Object[][] { + { Level.NONE, Arrays.asList() }, + { Level.BASIC, 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\\)") } + }); } - } - @Test(dataProvider = "levelToUnknownHostOutput") - public void unknownHostEmits(final Logger.Level logLevel, List expectedMessages) throws IOException, InterruptedException { + public RetryEmitsTest(Level logLevel, List expectedMessages) { + this.logLevel = logLevel; + logger.expectMessages(expectedMessages); + } - try { - SendsStuff api = Feign.create(SendsStuff.class, "http://robofu.abc", - new DontRetryModule(), new DefaultModule(logger, logLevel)); + @Test public void retryEmits() throws IOException, InterruptedException { - api.login("netflix", "denominator", "password"); + thrown.expect(FeignException.class); - fail(); - } catch (FeignException e) { - assertMessagesMatch(expectedMessages); - } - } + SendsStuff api = Feign.builder() + .logger(logger) + .logLevel(logLevel) + .retryer( new Retryer() { + boolean retried; - @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; + } + }) + .target(SendsStuff.class, "http://robofu.abc"); - @Override public void continueOrPropagate(RetryableException e) { - if (!retried) { - retried = true; - return; - } - throw e; - } - }; + api.login("netflix", "denominator", "password"); } } - 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"); + private static final class RecordingLogger extends Logger implements TestRule { + private final List messages = new ArrayList(); + private final List expectedMessages = new ArrayList(); - 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\\)" - )); + RecordingLogger expectMessages(List expectedMessages){ + this.expectedMessages.addAll(expectedMessages); + return this; + } + + @Override protected void log(String configKey, String format, Object... args) { + messages.add(methodTag(configKey) + String.format(format, args)); } - } - private void assertMessagesMatch(List 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)); + @Override public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override public void evaluate() throws Throwable { + base.evaluate(); + assertEquals(messages.size(), expectedMessages.size()); + for (int i = 0; i < messages.size(); i++) { + assertTrue("Didn't match at message " + (i + 1) + ":\n" + Joiner.on('\n').join(messages), + Pattern.compile(expectedMessages.get(i), Pattern.DOTALL).matcher(messages.get(i)).matches()); + } + } + }; } } } diff --git a/core/src/test/java/feign/RequestTemplateTest.java b/core/src/test/java/feign/RequestTemplateTest.java index bc1f31a8d2..6c873a048e 100644 --- a/core/src/test/java/feign/RequestTemplateTest.java +++ b/core/src/test/java/feign/RequestTemplateTest.java @@ -18,30 +18,30 @@ 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 org.junit.Test; import static feign.RequestTemplate.expand; -import static org.testng.Assert.assertEquals; +import static org.junit.Assert.assertEquals; public class RequestTemplateTest { @Test public void expandNotUrlEncoded() { for (String val : ImmutableList.of("apples", "sp ace", "unic???de", "qu?stion")) - assertEquals(expand("/users/{user}", ImmutableMap.of("user", val)), "/users/" + val); + assertEquals("/users/" + val, expand("/users/{user}", ImmutableMap.of("user", val))); } @Test public void expandMultipleParams() { - assertEquals(expand("/users/{user}/{repo}", ImmutableMap.of("user", "unic???de", "repo", "foo")), - "/users/unic???de/foo"); + assertEquals("/users/unic???de/foo", + expand("/users/{user}/{repo}", ImmutableMap.of("user", "unic???de", "repo", "foo"))); } @Test public void expandParamKeyHyphen() { - assertEquals(expand("/{user-dir}", ImmutableMap.of("user-dir", "foo")), "/foo"); + assertEquals("/foo", expand("/{user-dir}", ImmutableMap.of("user-dir", "foo"))); } @Test public void expandMissingParamProceeds() { - assertEquals(expand("/{user-dir}", ImmutableMap.of("user_dir", "foo")), "/{user-dir}"); + assertEquals("/{user-dir}", expand("/{user-dir}", ImmutableMap.of("user_dir", "foo"))); } @Test public void resolveTemplateWithParameterizedPathSkipsEncodingSlash() { @@ -49,40 +49,41 @@ public class RequestTemplateTest { RequestTemplate template = new RequestTemplate().method("GET") .append("{zoneId}"); - assertEquals(template.toString(), ""// - + "GET {zoneId} HTTP/1.1\n"); + assertEquals("GET {zoneId} HTTP/1.1\n", template.toString()); template.resolve(ImmutableMap.of("zoneId", "/hostedzone/Z1PA6795UKMFR9")); - assertEquals(template.toString(), ""// - + "GET /hostedzone/Z1PA6795UKMFR9 HTTP/1.1\n"); + assertEquals("GET /hostedzone/Z1PA6795UKMFR9 HTTP/1.1\n", template.toString()); template.insert(0, "https://route53.amazonaws.com/2012-12-12"); - assertEquals(template.request().toString(), ""// - + "GET https://route53.amazonaws.com/2012-12-12/hostedzone/Z1PA6795UKMFR9 HTTP/1.1\n"); + assertEquals("GET https://route53.amazonaws.com/2012-12-12/hostedzone/Z1PA6795UKMFR9 HTTP/1.1\n", + template.request().toString()); } @Test public void resolveTemplateWithBaseAndParameterizedQuery() { RequestTemplate template = new RequestTemplate().method("GET") .append("/?Action=DescribeRegions").query("RegionName.1", "{region}"); - assertEquals(template.queries(), - ImmutableListMultimap.of("Action", "DescribeRegions", "RegionName.1", "{region}").asMap()); - assertEquals(template.toString(), ""// - + "GET /?Action=DescribeRegions&RegionName.1={region} HTTP/1.1\n"); + assertEquals( + ImmutableListMultimap.of("Action", "DescribeRegions", "RegionName.1", "{region}").asMap(), + template.queries()); + assertEquals("GET /?Action=DescribeRegions&RegionName.1={region} HTTP/1.1\n", + template.toString()); template.resolve(ImmutableMap.of("region", "eu-west-1")); - assertEquals(template.queries(), - ImmutableListMultimap.of("Action", "DescribeRegions", "RegionName.1", "eu-west-1").asMap()); + assertEquals( + ImmutableListMultimap.of("Action", "DescribeRegions", "RegionName.1", "eu-west-1").asMap(), + template.queries()); - assertEquals(template.toString(), ""// - + "GET /?Action=DescribeRegions&RegionName.1=eu-west-1 HTTP/1.1\n"); + assertEquals("GET /?Action=DescribeRegions&RegionName.1=eu-west-1 HTTP/1.1\n", + template.toString()); template.insert(0, "https://iam.amazonaws.com"); - assertEquals(template.request().toString(), ""// - + "GET https://iam.amazonaws.com/?Action=DescribeRegions&RegionName.1=eu-west-1 HTTP/1.1\n"); + assertEquals( + "GET https://iam.amazonaws.com/?Action=DescribeRegions&RegionName.1=eu-west-1 HTTP/1.1\n", + template.request().toString()); } @Test public void resolveTemplateWithBaseAndParameterizedIterableQuery() { @@ -96,7 +97,7 @@ ImmutableListMultimap. builder() .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"); + assertEquals("GET /?Query=one&Queries=us-east-1&Queries=eu-west-1 HTTP/1.1\n", template.toString()); } @Test public void resolveTemplateWithMixedRequestLineParams() throws Exception { @@ -112,14 +113,15 @@ ImmutableListMultimap. builder() .build() ); - assertEquals(template.toString(), ""// - + "GET /domains/1001/records?name=denominator.io&type=CNAME HTTP/1.1\n"); + assertEquals("GET /domains/1001/records?name=denominator.io&type=CNAME HTTP/1.1\n", + template.toString()); template.insert(0, "https://dns.api.rackspacecloud.com/v1.0/1234"); - assertEquals(template.request().toString(), ""// + assertEquals(""// + "GET https://dns.api.rackspacecloud.com/v1.0/1234"// - + "/domains/1001/records?name=denominator.io&type=CNAME HTTP/1.1\n"); + + "/domains/1001/records?name=denominator.io&type=CNAME HTTP/1.1\n", + template.request().toString()); } @Test public void insertHasQueryParams() throws Exception { @@ -135,13 +137,13 @@ ImmutableListMultimap. builder() .build() ); - assertEquals(template.toString(), ""// - + "GET /domains/1001/records?name=denominator.io&type=CNAME HTTP/1.1\n"); + assertEquals("GET /domains/1001/records?name=denominator.io&type=CNAME HTTP/1.1\n", + template.toString()); 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"); + assertEquals("GET https://host/v1.0/1234/domains/1001/records?provider=foo&name=denominator.io&type=CNAME HTTP/1.1\n", + template.request().toString()); } @Test public void resolveTemplateWithBodyTemplateSetsBodyAndContentLength() { @@ -156,19 +158,21 @@ ImmutableListMultimap. builder() .build() ); - assertEquals(template.toString(), ""// + assertEquals(""// + "POST HTTP/1.1\n"// + "Content-Length: 80\n"// + "\n"// - + "{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}"); + + "{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}", + template.toString()); template.insert(0, "https://api2.dynect.net/REST"); - assertEquals(template.request().toString(), ""// + assertEquals(""// + "POST https://api2.dynect.net/REST HTTP/1.1\n" // + "Content-Length: 80\n" // + "\n" // - + "{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}"); + + "{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}", + template.request().toString()); } @Test public void skipUnresolvedQueries() throws Exception { @@ -183,8 +187,8 @@ ImmutableListMultimap. builder() .build() ); - assertEquals(template.toString(), ""// - + "GET /domains/1001/records?name=denominator.io HTTP/1.1\n"); + assertEquals("GET /domains/1001/records?name=denominator.io HTTP/1.1\n", + template.toString()); } @Test public void allQueriesUnresolvable() throws Exception { @@ -198,7 +202,6 @@ ImmutableListMultimap. builder() .build() ); - assertEquals(template.toString(), ""// - + "GET /domains/1001/records HTTP/1.1\n"); + assertEquals("GET /domains/1001/records HTTP/1.1\n", template.toString()); } } diff --git a/core/src/test/java/feign/UtilTest.java b/core/src/test/java/feign/UtilTest.java index 72fd77b201..c7de8ae85a 100644 --- a/core/src/test/java/feign/UtilTest.java +++ b/core/src/test/java/feign/UtilTest.java @@ -16,16 +16,14 @@ package feign; import feign.codec.Decoder; -import org.testng.annotations.Test; - import java.io.Reader; import java.lang.reflect.Type; import java.util.List; +import org.junit.Test; import static feign.Util.resolveLastTypeParameter; -import static org.testng.Assert.assertEquals; +import static org.junit.Assert.assertEquals; -@Test public class UtilTest { interface LastTypeParameter { @@ -49,38 +47,38 @@ static class ParameterizedSubtype implements Parameterized { Type context = LastTypeParameter.class.getDeclaredField("PARAMETERIZED_LIST_STRING").getGenericType(); Type listStringType = LastTypeParameter.class.getDeclaredField("LIST_STRING").getGenericType(); Type last = resolveLastTypeParameter(context, Parameterized.class); - assertEquals(last, listStringType); + assertEquals(listStringType, last); } @Test public void lastTypeFromInstance() throws Exception { Parameterized instance = new ParameterizedSubtype(); Type last = resolveLastTypeParameter(instance.getClass(), Parameterized.class); - assertEquals(last, String.class); + assertEquals(String.class, last); } @Test public void lastTypeFromAnonymous() throws Exception { Parameterized instance = new Parameterized() {}; Type last = resolveLastTypeParameter(instance.getClass(), Parameterized.class); - assertEquals(last, Reader.class); + assertEquals(Reader.class, last); } @Test public void resolveLastTypeParameterWhenWildcard() throws Exception { Type context = LastTypeParameter.class.getDeclaredField("PARAMETERIZED_WILDCARD_LIST_STRING").getGenericType(); Type listStringType = LastTypeParameter.class.getDeclaredField("LIST_STRING").getGenericType(); Type last = resolveLastTypeParameter(context, Parameterized.class); - assertEquals(last, listStringType); + assertEquals(listStringType, last); } @Test public void resolveLastTypeParameterWhenParameterizedSubtype() throws Exception { Type context = LastTypeParameter.class.getDeclaredField("PARAMETERIZED_DECODER_LIST_STRING").getGenericType(); Type listStringType = LastTypeParameter.class.getDeclaredField("LIST_STRING").getGenericType(); Type last = resolveLastTypeParameter(context, ParameterizedDecoder.class); - assertEquals(last, listStringType); + assertEquals(listStringType, last); } @Test public void unboundWildcardIsObject() throws Exception { Type context = LastTypeParameter.class.getDeclaredField("PARAMETERIZED_DECODER_UNBOUND").getGenericType(); Type last = resolveLastTypeParameter(context, ParameterizedDecoder.class); - assertEquals(last, Object.class); + assertEquals(Object.class, last); } } diff --git a/core/src/test/java/feign/auth/BasicAuthRequestInterceptorTest.java b/core/src/test/java/feign/auth/BasicAuthRequestInterceptorTest.java index 9b16527620..56745b0ccf 100644 --- a/core/src/test/java/feign/auth/BasicAuthRequestInterceptorTest.java +++ b/core/src/test/java/feign/auth/BasicAuthRequestInterceptorTest.java @@ -16,12 +16,10 @@ package feign.auth; import feign.RequestTemplate; -import org.testng.annotations.Test; - -import java.util.Collection; import java.util.Collections; +import org.junit.Test; -import static org.testng.Assert.assertEquals; +import static org.junit.Assert.assertEquals; /** * Tests for {@link BasicAuthRequestInterceptor}. @@ -34,9 +32,8 @@ public class BasicAuthRequestInterceptorTest { RequestTemplate template = new RequestTemplate(); BasicAuthRequestInterceptor interceptor = new BasicAuthRequestInterceptor("Aladdin", "open sesame"); interceptor.apply(template); - Collection actualValue = template.headers().get("Authorization"); - Collection expectedValue = Collections.singletonList("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="); - assertEquals(actualValue, expectedValue); + assertEquals(Collections.singletonList("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="), + template.headers().get("Authorization")); } /** @@ -47,9 +44,8 @@ public class BasicAuthRequestInterceptorTest { BasicAuthRequestInterceptor interceptor = new BasicAuthRequestInterceptor("IOIOIOIOIOIOIOIOIOIOIOIOIOIOIOIOIOIOIO", "101010101010101010101010101010101010101010"); interceptor.apply(template); - Collection actualValue = template.headers().get("Authorization"); - Collection expectedValue = Collections. - singletonList("Basic SU9JT0lPSU9JT0lPSU9JT0lPSU9JT0lPSU9JT0lPSU9JT0lPSU86MTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEw"); - assertEquals(actualValue, expectedValue); + assertEquals(Collections.singletonList( + "Basic SU9JT0lPSU9JT0lPSU9JT0lPSU9JT0lPSU9JT0lPSU9JT0lPSU86MTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEw"), + template.headers().get("Authorization")); } } diff --git a/core/src/test/java/feign/codec/DefaultDecoderTest.java b/core/src/test/java/feign/codec/DefaultDecoderTest.java index e270df5b53..c15057706d 100644 --- a/core/src/test/java/feign/codec/DefaultDecoderTest.java +++ b/core/src/test/java/feign/codec/DefaultDecoderTest.java @@ -16,44 +16,48 @@ package feign.codec; import feign.Response; -import org.testng.annotations.Test; -import org.w3c.dom.Document; - import java.io.ByteArrayInputStream; import java.io.InputStream; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; - -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertNull; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.w3c.dom.Document; import static feign.Util.UTF_8; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; public class DefaultDecoderTest { + @Rule public final ExpectedException thrown = ExpectedException.none(); + private final Decoder decoder = new Decoder.Default(); @Test public void testDecodesToString() throws Exception { Response response = knownResponse(); Object decodedObject = decoder.decode(response, String.class); - assertEquals(decodedObject.getClass(), String.class); - assertEquals(decodedObject.toString(), "response body"); + assertEquals(String.class, decodedObject.getClass()); + assertEquals("response body", decodedObject.toString()); } @Test public void testDecodesToByteArray() throws Exception { Response response = knownResponse(); Object decodedObject = decoder.decode(response, byte[].class); - assertEquals(decodedObject.getClass(), byte[].class); - assertEquals((byte[]) decodedObject, "response body".getBytes(UTF_8)); + assertEquals(byte[].class, decodedObject.getClass()); + assertEquals("response body", new String((byte[]) decodedObject, UTF_8)); } @Test public void testDecodesNullBodyToNull() throws Exception { assertNull(decoder.decode(nullBodyResponse(), Document.class)); } - @Test(expectedExceptions = DecodeException.class, expectedExceptionsMessageRegExp = ".* is not a type supported by this decoder.") - public void testRefusesToDecodeOtherTypes() throws Exception { + @Test public void testRefusesToDecodeOtherTypes() throws Exception { + thrown.expect(DecodeException.class); + thrown.expectMessage(" is not a type supported by this decoder."); + decoder.decode(knownResponse(), Document.class); } diff --git a/core/src/test/java/feign/codec/DefaultEncoderTest.java b/core/src/test/java/feign/codec/DefaultEncoderTest.java index 1dc4fe5985..1b643aa940 100644 --- a/core/src/test/java/feign/codec/DefaultEncoderTest.java +++ b/core/src/test/java/feign/codec/DefaultEncoderTest.java @@ -16,33 +16,39 @@ package feign.codec; import feign.RequestTemplate; -import org.testng.annotations.Test; - +import java.util.Arrays; import java.util.Date; - -import static org.testng.Assert.assertEquals; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; import static feign.Util.UTF_8; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; public class DefaultEncoderTest { + @Rule public final ExpectedException thrown = ExpectedException.none(); + private final Encoder encoder = new Encoder.Default(); @Test public void testEncodesStrings() throws Exception { String content = "This is my content"; RequestTemplate template = new RequestTemplate(); encoder.encode(content, template); - assertEquals(template.body(), content.getBytes(UTF_8)); + assertEquals(content, new String(template.body(), UTF_8)); } @Test public void testEncodesByteArray() throws Exception { byte[] content = {12, 34, 56}; RequestTemplate template = new RequestTemplate(); encoder.encode(content, template); - assertEquals(template.body(), content); + assertTrue(Arrays.equals(content, template.body())); } - @Test(expectedExceptions = EncodeException.class, expectedExceptionsMessageRegExp = ".* is not a type supported by this encoder.") - public void testRefusesToEncodeOtherTypes() throws Exception { + @Test public void testRefusesToEncodeOtherTypes() throws Exception { + thrown.expect(EncodeException.class); + thrown.expectMessage("is not a type supported by this encoder."); + encoder.encode(new Date(), new RequestTemplate()); } } diff --git a/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java b/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java index e6173bca6c..f3814389a7 100644 --- a/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java +++ b/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java @@ -17,39 +17,45 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultimap; - -import org.testng.annotations.Test; - -import java.util.Collection; - import feign.FeignException; import feign.Response; -import feign.RetryableException; +import java.util.Collection; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; import static feign.Util.RETRY_AFTER; import static feign.Util.UTF_8; public class DefaultErrorDecoderTest { + @Rule public final ExpectedException thrown = ExpectedException.none(); + ErrorDecoder errorDecoder = new ErrorDecoder.Default(); - @Test(expectedExceptions = FeignException.class, expectedExceptionsMessageRegExp = "status 500 reading Service#foo\\(\\)") - public void throwsFeignException() throws Throwable { + @Test public void throwsFeignException() throws Throwable { + thrown.expect(FeignException.class); + thrown.expectMessage("status 500 reading Service#foo()"); + Response response = Response.create(500, "Internal server error", ImmutableMap.>of(), null); throw errorDecoder.decode("Service#foo()", response); } - @Test(expectedExceptions = FeignException.class, expectedExceptionsMessageRegExp = "status 500 reading Service#foo\\(\\); content:\nhello world") - public void throwsFeignExceptionIncludingBody() throws Throwable { + @Test public void throwsFeignExceptionIncludingBody() throws Throwable { + thrown.expect(FeignException.class); + thrown.expectMessage("status 500 reading Service#foo(); content:\nhello world"); + Response response = Response.create(500, "Internal server error", ImmutableMap.>of(), "hello world", UTF_8); throw errorDecoder.decode("Service#foo()", response); } - @Test(expectedExceptions = RetryableException.class, expectedExceptionsMessageRegExp = "status 503 reading Service#foo\\(\\)") - public void retryAfterHeaderThrowsRetryableException() throws Throwable { + @Test public void retryAfterHeaderThrowsRetryableException() throws Throwable { + thrown.expect(FeignException.class); + thrown.expectMessage("status 503 reading Service#foo()"); + Response response = Response.create(503, "Service Unavailable", ImmutableMultimap.of(RETRY_AFTER, "Sat, 1 Jan 2000 00:00:00 GMT").asMap(), null); diff --git a/core/src/test/java/feign/codec/RetryAfterDecoderTest.java b/core/src/test/java/feign/codec/RetryAfterDecoderTest.java index 7f4e4fbaca..06ba5496cc 100644 --- a/core/src/test/java/feign/codec/RetryAfterDecoderTest.java +++ b/core/src/test/java/feign/codec/RetryAfterDecoderTest.java @@ -15,16 +15,14 @@ */ package feign.codec; -import org.testng.annotations.Test; - -import java.text.ParseException; - import feign.codec.ErrorDecoder.RetryAfterDecoder; +import java.text.ParseException; +import org.junit.Test; import static feign.codec.ErrorDecoder.RetryAfterDecoder.RFC822_FORMAT; import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertFalse; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; public class RetryAfterDecoderTest { @@ -33,12 +31,12 @@ public class RetryAfterDecoderTest { } @Test public void rfc822Parses() throws ParseException { - assertEquals(decoder.apply("Fri, 31 Dec 1999 23:59:59 GMT"), - RFC822_FORMAT.parse("Fri, 31 Dec 1999 23:59:59 GMT")); + assertEquals(RFC822_FORMAT.parse("Fri, 31 Dec 1999 23:59:59 GMT"), + decoder.apply("Fri, 31 Dec 1999 23:59:59 GMT")); } @Test public void relativeSecondsParses() throws ParseException { - assertEquals(decoder.apply("86400"), RFC822_FORMAT.parse("Sun, 2 Jan 2000 00:00:00 GMT")); + assertEquals(RFC822_FORMAT.parse("Sun, 2 Jan 2000 00:00:00 GMT"), decoder.apply("86400")); } private RetryAfterDecoder decoder = new RetryAfterDecoder(RFC822_FORMAT) { diff --git a/gson/build.gradle b/gson/build.gradle index 6e6252cbd3..c0a064acd2 100644 --- a/gson/build.gradle +++ b/gson/build.gradle @@ -2,12 +2,8 @@ apply plugin: 'java' sourceCompatibility = 1.6 -test { - useTestNG() -} - dependencies { compile project(':feign-core') compile 'com.google.code.gson:gson:2.2.4' - testCompile 'org.testng:testng:6.8.5' + testCompile 'junit:junit:4.12' } diff --git a/gson/src/test/java/feign/gson/GsonModuleTest.java b/gson/src/test/java/feign/gson/GsonModuleTest.java index d0bce2abfc..341a4c3519 100644 --- a/gson/src/test/java/feign/gson/GsonModuleTest.java +++ b/gson/src/test/java/feign/gson/GsonModuleTest.java @@ -26,9 +26,6 @@ import feign.Response; import feign.codec.Decoder; import feign.codec.Encoder; -import org.testng.annotations.Test; - -import javax.inject.Inject; import java.io.IOException; import java.util.Arrays; import java.util.Collection; @@ -37,12 +34,13 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; - -import static org.testng.Assert.assertEquals; +import javax.inject.Inject; +import org.junit.Test; import static feign.Util.UTF_8; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; -@Test public class GsonModuleTest { @Module(includes = GsonModule.class, injects = EncoderAndDecoderBindings.class) static class EncoderAndDecoderBindings { @@ -54,8 +52,8 @@ static class EncoderAndDecoderBindings { EncoderAndDecoderBindings bindings = new EncoderAndDecoderBindings(); ObjectGraph.create(bindings).inject(bindings); - assertEquals(bindings.encoder.getClass(), GsonEncoder.class); - assertEquals(bindings.decoder.getClass(), GsonDecoder.class); + assertEquals(GsonEncoder.class, bindings.encoder.getClass()); + assertEquals(GsonDecoder.class, bindings.decoder.getClass()); } @Module(includes = GsonModule.class, injects = EncoderBindings.class) @@ -77,7 +75,7 @@ static class EncoderBindings { RequestTemplate template = new RequestTemplate(); bindings.encoder.encode(map, template); - assertEquals(template.body(), expectedBody.getBytes(UTF_8)); + assertEquals(expectedBody, new String(template.body(), UTF_8)); } @Test public void encodesFormParams() throws Exception { @@ -99,7 +97,7 @@ static class EncoderBindings { RequestTemplate template = new RequestTemplate(); bindings.encoder.encode(form, template); - assertEquals(template.body(), expectedBody.getBytes(UTF_8)); + assertEquals(expectedBody, new String(template.body(), UTF_8)); } static class Zone extends LinkedHashMap { @@ -135,8 +133,8 @@ static class DecoderBindings { Response response = Response.create(200, "OK", Collections.>emptyMap(), zonesJson, UTF_8); - assertEquals(bindings.decoder.decode(response, new TypeToken>() { - }.getType()), zones); + assertEquals(zones, bindings.decoder.decode(response, new TypeToken>() { + }.getType())); } @Test public void nullBodyDecodesToNull() throws Exception { @@ -144,7 +142,7 @@ static class DecoderBindings { ObjectGraph.create(bindings).inject(bindings); Response response = Response.create(204, "OK", Collections.>emptyMap(), null); - assertEquals(bindings.decoder.decode(response, String.class), null); + assertNull(bindings.decoder.decode(response, String.class)); } private String zonesJson = ""// @@ -192,7 +190,7 @@ static class CustomTypeAdapter { Response response = Response.create(200, "OK", Collections.>emptyMap(), zonesJson, UTF_8); - assertEquals(bindings.decoder.decode(response, new TypeToken>() { - }.getType()), zones); + assertEquals(zones, bindings.decoder.decode(response, new TypeToken>() { + }.getType())); } } diff --git a/jackson/build.gradle b/jackson/build.gradle index edd2e0d4d4..ca2414cac9 100644 --- a/jackson/build.gradle +++ b/jackson/build.gradle @@ -2,13 +2,9 @@ apply plugin: 'java' sourceCompatibility = 1.6 -test { - useTestNG() -} - dependencies { compile project(':feign-core') compile 'com.fasterxml.jackson.core:jackson-databind:2.2.2' - testCompile 'org.testng:testng:6.8.5' + testCompile 'junit:junit:4.12' testCompile 'com.google.guava:guava:14.0.1' } diff --git a/jackson/src/test/java/feign/jackson/JacksonModuleTest.java b/jackson/src/test/java/feign/jackson/JacksonModuleTest.java index a4f9dfa8ef..22bcb2251d 100644 --- a/jackson/src/test/java/feign/jackson/JacksonModuleTest.java +++ b/jackson/src/test/java/feign/jackson/JacksonModuleTest.java @@ -13,17 +13,21 @@ import feign.Response; import feign.codec.Decoder; import feign.codec.Encoder; -import org.testng.annotations.Test; - -import javax.inject.Inject; import java.io.IOException; -import java.util.*; - -import static org.testng.Assert.assertEquals; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import javax.inject.Inject; +import org.junit.Test; import static feign.Util.UTF_8; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; -@Test public class JacksonModuleTest { @Module(includes = JacksonModule.class, injects = EncoderAndDecoderBindings.class) static class EncoderAndDecoderBindings { @@ -38,8 +42,8 @@ public void providesEncoderDecoder() throws Exception { EncoderAndDecoderBindings bindings = new EncoderAndDecoderBindings(); ObjectGraph.create(bindings).inject(bindings); - assertEquals(bindings.encoder.getClass(), JacksonEncoder.class); - assertEquals(bindings.decoder.getClass(), JacksonDecoder.class); + assertEquals(JacksonEncoder.class, bindings.encoder.getClass()); + assertEquals(JacksonDecoder.class, bindings.decoder.getClass()); } @Module(includes = JacksonModule.class, injects = EncoderBindings.class) @@ -56,10 +60,10 @@ static class EncoderBindings { RequestTemplate template = new RequestTemplate(); bindings.encoder.encode(map, template); - assertEquals(new String(template.body(), UTF_8), ""// + assertEquals(""// + "{\n" // + " \"foo\" : 1\n" // - + "}"); + + "}", new String(template.body(), UTF_8)); } @Test public void encodesFormParams() throws Exception { @@ -72,11 +76,11 @@ static class EncoderBindings { RequestTemplate template = new RequestTemplate(); bindings.encoder.encode(form, template); - assertEquals(new String(template.body(), UTF_8), ""// + assertEquals(""// + "{\n" // + " \"foo\" : 1,\n" // + " \"bar\" : [ 2, 3 ]\n" // - + "}"); + + "}", new String(template.body(), UTF_8)); } static class Zone extends LinkedHashMap { @@ -113,8 +117,8 @@ static class DecoderBindings { Response response = Response.create(200, "OK", Collections.>emptyMap(), zonesJson, UTF_8); - assertEquals(bindings.decoder.decode(response, new TypeToken>() { - }.getType()), zones); + assertEquals(zones, bindings.decoder.decode(response, new TypeToken>() { + }.getType())); } @Test public void nullBodyDecodesToNull() throws Exception { @@ -122,7 +126,7 @@ static class DecoderBindings { ObjectGraph.create(bindings).inject(bindings); Response response = Response.create(204, "OK", Collections.>emptyMap(), null); - assertEquals(bindings.decoder.decode(response, String.class), null); + assertNull(bindings.decoder.decode(response, String.class)); } private String zonesJson = ""// @@ -182,7 +186,7 @@ com.fasterxml.jackson.databind.Module upperZone() { Response response = Response.create(200, "OK", Collections.>emptyMap(), zonesJson, UTF_8); - assertEquals(bindings.decoder.decode(response, new TypeToken>() { - }.getType()), zones); + assertEquals(zones, bindings.decoder.decode(response, new TypeToken>() { + }.getType())); } } diff --git a/jaxb/build.gradle b/jaxb/build.gradle index 1053548149..4dda750faa 100644 --- a/jaxb/build.gradle +++ b/jaxb/build.gradle @@ -1,11 +1,7 @@ apply plugin: 'java' -test { - useTestNG() -} - dependencies { compile project(':feign-core') - testCompile 'org.testng:testng:6.8.5' + testCompile 'junit:junit:4.12' testCompile 'com.google.guava:guava:14.0.1' } diff --git a/jaxb/src/test/java/feign/jaxb/JAXBContextFactoryTest.java b/jaxb/src/test/java/feign/jaxb/JAXBContextFactoryTest.java index b7544cc0fe..b9ffbd308c 100644 --- a/jaxb/src/test/java/feign/jaxb/JAXBContextFactoryTest.java +++ b/jaxb/src/test/java/feign/jaxb/JAXBContextFactoryTest.java @@ -15,12 +15,11 @@ */ package feign.jaxb; -import org.testng.annotations.Test; - import javax.xml.bind.Marshaller; +import org.junit.Test; -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; public class JAXBContextFactoryTest { @Test @@ -30,7 +29,7 @@ public void buildsMarshallerWithJAXBEncodingProperty() throws Exception { .build(); Marshaller marshaller = factory.createMarshaller(Object.class); - assertEquals(marshaller.getProperty(Marshaller.JAXB_ENCODING), "UTF-16"); + assertEquals("UTF-16", marshaller.getProperty(Marshaller.JAXB_ENCODING)); } @Test @@ -40,8 +39,8 @@ public void buildsMarshallerWithSchemaLocationProperty() throws Exception { .build(); Marshaller marshaller = factory.createMarshaller(Object.class); - assertEquals(marshaller.getProperty(Marshaller.JAXB_SCHEMA_LOCATION), - "http://apihost http://apihost/schema.xsd"); + assertEquals("http://apihost http://apihost/schema.xsd", + marshaller.getProperty(Marshaller.JAXB_SCHEMA_LOCATION)); } @Test @@ -51,7 +50,8 @@ public void buildsMarshallerWithNoNamespaceSchemaLocationProperty() throws Excep .build(); Marshaller marshaller = factory.createMarshaller(Object.class); - assertEquals(marshaller.getProperty(Marshaller.JAXB_NO_NAMESPACE_SCHEMA_LOCATION), "http://apihost/schema.xsd"); + assertEquals("http://apihost/schema.xsd", + marshaller.getProperty(Marshaller.JAXB_NO_NAMESPACE_SCHEMA_LOCATION)); } @Test diff --git a/jaxb/src/test/java/feign/jaxb/JAXBModuleTest.java b/jaxb/src/test/java/feign/jaxb/JAXBModuleTest.java index 104d66d080..ec40f104f9 100644 --- a/jaxb/src/test/java/feign/jaxb/JAXBModuleTest.java +++ b/jaxb/src/test/java/feign/jaxb/JAXBModuleTest.java @@ -22,7 +22,6 @@ import feign.Response; import feign.codec.Decoder; import feign.codec.Encoder; -import org.testng.annotations.Test; import javax.inject.Inject; import javax.xml.bind.annotation.XmlAccessType; @@ -31,11 +30,11 @@ import javax.xml.bind.annotation.XmlRootElement; import java.util.Collection; import java.util.Collections; +import org.junit.Test; import static feign.Util.UTF_8; -import static org.testng.Assert.assertEquals; +import static org.junit.Assert.assertEquals; -@Test public class JAXBModuleTest { @Module(includes = JAXBModule.class, injects = EncoderAndDecoderBindings.class) static class EncoderAndDecoderBindings { @@ -61,8 +60,8 @@ public void providesEncoderDecoder() throws Exception { EncoderAndDecoderBindings bindings = new EncoderAndDecoderBindings(); ObjectGraph.create(bindings).inject(bindings); - assertEquals(bindings.encoder.getClass(), JAXBEncoder.class); - assertEquals(bindings.decoder.getClass(), JAXBDecoder.class); + assertEquals(JAXBEncoder.class, bindings.encoder.getClass()); + assertEquals(JAXBDecoder.class, bindings.decoder.getClass()); } @XmlRootElement @@ -109,8 +108,9 @@ public void encodesXml() throws Exception { RequestTemplate template = new RequestTemplate(); bindings.encoder.encode(mock, template); - assertEquals(new String(template.body(), UTF_8), "Test"); + assertEquals( + "Test", + new String(template.body(), UTF_8)); } @Test @@ -128,8 +128,9 @@ public void encodesXmlWithCustomJAXBEncoding() throws Exception { RequestTemplate template = new RequestTemplate(); encoder.encode(mock, template); - assertEquals(new String(template.body(), UTF_8), "Test"); + assertEquals("Test", + new String(template.body(), UTF_8)); } @Test @@ -147,10 +148,10 @@ public void encodesXmlWithCustomJAXBSchemaLocation() throws Exception { RequestTemplate template = new RequestTemplate(); encoder.encode(mock, template); - assertEquals(new String(template.body(), UTF_8), "" + - "Test"); + "Test", new String(template.body(), UTF_8)); } @Test @@ -168,10 +169,10 @@ public void encodesXmlWithCustomJAXBNoNamespaceSchemaLocation() throws Exception RequestTemplate template = new RequestTemplate(); encoder.encode(mock, template); - assertEquals(new String(template.body(), UTF_8), "" + - "Test"); + "Test", new String(template.body(), UTF_8)); } @Test @@ -197,7 +198,7 @@ public void encodesXmlWithCustomJAXBFormattedOutput() { .append(" Test").append(NEWLINE) .append("").append(NEWLINE); - assertEquals(new String(template.body(), UTF_8), expectedXml.toString()); + assertEquals(expectedXml.toString(), new String(template.body(), UTF_8)); } @Test @@ -214,6 +215,6 @@ public void decodesXml() throws Exception { Response response = Response.create(200, "OK", Collections.>emptyMap(), mockXml, UTF_8); - assertEquals(bindings.decoder.decode(response, new TypeToken() {}.getType()), mock); + assertEquals(mock, bindings.decoder.decode(response, new TypeToken() {}.getType())); } } diff --git a/jaxrs/build.gradle b/jaxrs/build.gradle index a3f6b1ac08..fdb8f2dadb 100644 --- a/jaxrs/build.gradle +++ b/jaxrs/build.gradle @@ -2,14 +2,10 @@ apply plugin: 'java' sourceCompatibility = 1.6 -test { - useTestNG() -} - dependencies { compile project(':feign-core') compile 'javax.ws.rs:jsr311-api:1.1.1' testCompile project(':feign-gson') testCompile 'com.google.guava:guava:14.0.1' - testCompile 'org.testng:testng:6.8.5' + testCompile 'junit:junit:4.12' } diff --git a/jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java b/jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java index 9a16e6c9c7..c9bd9878f2 100644 --- a/jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java +++ b/jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java @@ -15,13 +15,16 @@ */ package feign.jaxrs; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; import com.google.gson.reflect.TypeToken; import feign.MethodMetadata; import feign.Response; -import org.testng.annotations.Test; - +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.net.URI; +import java.util.Arrays; +import java.util.List; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.FormParam; @@ -34,12 +37,9 @@ import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import java.net.URI; -import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; import static feign.jaxrs.JAXRSModule.ACCEPT; import static feign.jaxrs.JAXRSModule.CONTENT_TYPE; @@ -49,17 +49,18 @@ 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.assertNull; -import static org.testng.Assert.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; /** * Tests interfaces defined per {@link feign.jaxrs.JAXRSModule.JAXRSContract} are interpreted into expected {@link feign * .RequestTemplate template} * instances. */ -@Test public class JAXRSContractTest { + @Rule public final ExpectedException thrown = ExpectedException.none(); + JAXRSModule.JAXRSContract contract = new JAXRSModule.JAXRSContract(); interface Methods { @@ -73,11 +74,10 @@ interface Methods { } @Test public void httpMethods() throws Exception { - assertEquals(contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("post")).template().method(), - POST); - assertEquals(contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("put")).template().method(), PUT); - assertEquals(contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("get")).template().method(), GET); - assertEquals(contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("delete")).template().method(), DELETE); + assertEquals(POST, contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("post")).template().method()); + assertEquals(PUT, contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("put")).template().method()); + assertEquals(GET, contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("get")).template().method()); + assertEquals(DELETE, contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("delete")).template().method()); } interface CustomMethodAndURIParam { @@ -93,13 +93,13 @@ interface CustomMethodAndURIParam { @Test public void requestLineOnlyRequiresMethod() throws Exception { MethodMetadata md = contract.parseAndValidatateMetadata(CustomMethodAndURIParam.class.getDeclaredMethod("patch", URI.class)); - assertEquals(md.template().method(), "PATCH"); - assertEquals(md.template().url(), ""); + assertEquals("PATCH", md.template().method()); + assertEquals("", md.template().url()); assertTrue(md.template().queries().isEmpty()); assertTrue(md.template().headers().isEmpty()); assertNull(md.template().body()); assertNull(md.template().bodyTemplate()); - assertEquals(md.urlIndex(), Integer.valueOf(0)); + assertEquals(Integer.valueOf(0), md.urlIndex()); } interface WithQueryParamsInPath { @@ -117,38 +117,38 @@ interface WithQueryParamsInPath { @Test public void queryParamsInPathExtract() throws Exception { { MethodMetadata md = contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("none")); - assertEquals(md.template().url(), "/"); + assertEquals("/", md.template().url()); assertTrue(md.template().queries().isEmpty()); - assertEquals(md.template().toString(), "GET / HTTP/1.1\n"); + assertEquals("GET / HTTP/1.1\n", md.template().toString()); } { MethodMetadata md = contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("one")); - assertEquals(md.template().url(), "/"); - assertEquals(md.template().queries().get("Action"), ImmutableSet.of("GetUser")); - assertEquals(md.template().toString(), "GET /?Action=GetUser HTTP/1.1\n"); + assertEquals("/", md.template().url()); + assertEquals(Arrays.asList("GetUser"), md.template().queries().get("Action")); + assertEquals("GET /?Action=GetUser HTTP/1.1\n", md.template().toString()); } { MethodMetadata md = contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("two")); - assertEquals(md.template().url(), "/"); - assertEquals(md.template().queries().get("Action"), ImmutableSet.of("GetUser")); - assertEquals(md.template().queries().get("Version"), ImmutableSet.of("2010-05-08")); - assertEquals(md.template().toString(), "GET /?Action=GetUser&Version=2010-05-08 HTTP/1.1\n"); + assertEquals("/", md.template().url()); + assertEquals(Arrays.asList("GetUser"), md.template().queries().get("Action")); + assertEquals(Arrays.asList("2010-05-08"), md.template().queries().get("Version")); + assertEquals("GET /?Action=GetUser&Version=2010-05-08 HTTP/1.1\n", md.template().toString()); } { MethodMetadata md = contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("three")); - assertEquals(md.template().url(), "/"); - assertEquals(md.template().queries().get("Action"), ImmutableSet.of("GetUser")); - assertEquals(md.template().queries().get("Version"), ImmutableSet.of("2010-05-08")); - assertEquals(md.template().queries().get("limit"), ImmutableSet.of("1")); - assertEquals(md.template().toString(), "GET /?Action=GetUser&Version=2010-05-08&limit=1 HTTP/1.1\n"); + assertEquals("/", md.template().url()); + assertEquals(Arrays.asList("GetUser"), md.template().queries().get("Action")); + assertEquals(Arrays.asList("2010-05-08"), md.template().queries().get("Version")); + assertEquals(Arrays.asList("1"), md.template().queries().get("limit")); + assertEquals("GET /?Action=GetUser&Version=2010-05-08&limit=1 HTTP/1.1\n", md.template().toString()); } { MethodMetadata md = contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("empty")); - assertEquals(md.template().url(), "/"); + assertEquals("/", md.template().url()); assertTrue(md.template().queries().containsKey("flag")); - assertEquals(md.template().queries().get("Action"), ImmutableSet.of("GetUser")); - assertEquals(md.template().queries().get("Version"), ImmutableSet.of("2010-05-08")); - assertEquals(md.template().toString(), "GET /?flag&Action=GetUser&Version=2010-05-08 HTTP/1.1\n"); + assertEquals(Arrays.asList("GetUser"), md.template().queries().get("Action")); + assertEquals(Arrays.asList("2010-05-08"), md.template().queries().get("Version")); + assertEquals("GET /?flag&Action=GetUser&Version=2010-05-08 HTTP/1.1\n", md.template().toString()); } } @@ -168,31 +168,39 @@ interface ProducesAndConsumes { @Test public void producesAddsAcceptHeader() throws Exception { MethodMetadata md = contract.parseAndValidatateMetadata(ProducesAndConsumes.class.getDeclaredMethod("produces")); - assertEquals(md.template().headers().get(ACCEPT), ImmutableSet.of(APPLICATION_XML)); + assertEquals(Arrays.asList(APPLICATION_XML), md.template().headers().get(ACCEPT)); } - @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "Produces.value\\(\\) was empty on method producesNada") - public void producesNada() throws Exception { + @Test public void producesNada() throws Exception { + thrown.expect(IllegalStateException.class); + thrown.expectMessage("Produces.value() was empty on method producesNada"); + contract.parseAndValidatateMetadata(ProducesAndConsumes.class.getDeclaredMethod("producesNada")); } - @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "Produces.value\\(\\) was empty on method producesEmpty") - public void producesEmpty() throws Exception { + @Test public void producesEmpty() throws Exception { + thrown.expect(IllegalStateException.class); + thrown.expectMessage("Produces.value() was empty on method producesEmpty"); + 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)); + assertEquals(Arrays.asList(APPLICATION_JSON), md.template().headers().get(CONTENT_TYPE)); } - @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "Consumes.value\\(\\) was empty on method consumesNada") - public void consumesNada() throws Exception { + @Test public void consumesNada() throws Exception { + thrown.expect(IllegalStateException.class); + thrown.expectMessage("Consumes.value() was empty on method consumesNada"); + contract.parseAndValidatateMetadata(ProducesAndConsumes.class.getDeclaredMethod("consumesNada")); } - @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "Consumes.value\\(\\) was empty on method consumesEmpty") - public void consumesEmpty() throws Exception { + @Test public void consumesEmpty() throws Exception { + thrown.expect(IllegalStateException.class); + thrown.expectMessage("Consumes.value() was empty on method consumesEmpty"); + contract.parseAndValidatateMetadata(ProducesAndConsumes.class.getDeclaredMethod("consumesEmpty")); } @@ -208,13 +216,15 @@ interface BodyParams { assertNull(md.template().body()); assertNull(md.template().bodyTemplate()); assertNull(md.urlIndex()); - assertEquals(md.bodyIndex(), Integer.valueOf(0)); - assertEquals(md.bodyType(), new TypeToken>() { - }.getType()); + assertEquals(Integer.valueOf(0), md.bodyIndex()); + assertEquals(new TypeToken>() { + }.getType(), md.bodyType()); } - @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "Method has too many Body.*") - public void tooManyBodies() throws Exception { + @Test public void tooManyBodies() throws Exception { + thrown.expect(IllegalStateException.class); + thrown.expectMessage("Method has too many Body"); + contract.parseAndValidatateMetadata(BodyParams.class.getDeclaredMethod("tooMany", List.class, List.class)); } @@ -222,8 +232,10 @@ public void tooManyBodies() throws Exception { @GET Response base(); } - @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "Path.value\\(\\) was empty on type .*") - public void emptyPathOnType() throws Exception { + @Test public void emptyPathOnType() throws Exception { + thrown.expect(IllegalStateException.class); + thrown.expectMessage("Path.value() was empty on type "); + contract.parseAndValidatateMetadata(EmptyPathOnType.class.getDeclaredMethod("base")); } @@ -239,18 +251,22 @@ public void emptyPathOnType() throws Exception { @Test public void pathOnType() throws Exception { MethodMetadata md = contract.parseAndValidatateMetadata(PathOnType.class.getDeclaredMethod("base")); - assertEquals(md.template().url(), "/base"); + assertEquals("/base", md.template().url()); md = contract.parseAndValidatateMetadata(PathOnType.class.getDeclaredMethod("get")); - assertEquals(md.template().url(), "/base/specific"); + assertEquals("/base/specific", md.template().url()); } - @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "Path.value\\(\\) was empty on method emptyPath") - public void emptyPathOnMethod() throws Exception { + @Test public void emptyPathOnMethod() throws Exception { + thrown.expect(IllegalStateException.class); + thrown.expectMessage("Path.value() was empty on method emptyPath"); + contract.parseAndValidatateMetadata(PathOnType.class.getDeclaredMethod("emptyPath")); } - @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "PathParam.value\\(\\) was empty on parameter 0") - public void emptyPathParam() throws Exception { + @Test public void emptyPathParam() throws Exception { + thrown.expect(IllegalStateException.class); + thrown.expectMessage("PathParam.value() was empty on parameter 0"); + contract.parseAndValidatateMetadata(PathOnType.class.getDeclaredMethod("emptyPathParam", String.class)); } @@ -261,15 +277,15 @@ interface WithURIParam { @Test public void methodCanHaveUriParam() throws Exception { MethodMetadata md = contract.parseAndValidatateMetadata(WithURIParam.class.getDeclaredMethod("uriParam", String.class, URI.class, String.class)); - assertEquals(md.urlIndex(), Integer.valueOf(1)); + assertEquals(Integer.valueOf(1), md.urlIndex()); } @Test public void pathParamsParseIntoIndexToName() throws Exception { MethodMetadata md = contract.parseAndValidatateMetadata(WithURIParam.class.getDeclaredMethod("uriParam", String.class, URI.class, String.class)); - assertEquals(md.template().url(), "/{1}/{2}"); - assertEquals(md.indexToName().get(0), ImmutableSet.of("1")); - assertEquals(md.indexToName().get(2), ImmutableSet.of("2")); + assertEquals("/{1}/{2}", md.template().url()); + assertEquals(Arrays.asList("1"), md.indexToName().get(0)); + assertEquals(Arrays.asList("2"), md.indexToName().get(2)); } interface WithPathAndQueryParams { @@ -286,17 +302,19 @@ Response recordsByNameAndType(@PathParam("domainId") int id, @QueryParam("name") assertNull(md.template().body()); assertNull(md.template().bodyTemplate()); assertTrue(md.template().headers().isEmpty()); - assertEquals(md.template().url(), "/domains/{domainId}/records"); - assertEquals(md.template().queries().get("name"), ImmutableSet.of("{name}")); - assertEquals(md.template().queries().get("type"), ImmutableSet.of("{type}")); - assertEquals(md.indexToName().get(0), ImmutableSet.of("domainId")); - assertEquals(md.indexToName().get(1), ImmutableSet.of("name")); - assertEquals(md.indexToName().get(2), ImmutableSet.of("type")); - assertEquals(md.template().toString(), "GET /domains/{domainId}/records?name={name}&type={type} HTTP/1.1\n"); + assertEquals("/domains/{domainId}/records", md.template().url()); + assertEquals(Arrays.asList("{name}"), md.template().queries().get("name")); + assertEquals(Arrays.asList("{type}"), md.template().queries().get("type")); + assertEquals(Arrays.asList("domainId"), md.indexToName().get(0)); + assertEquals(Arrays.asList("name"), md.indexToName().get(1)); + assertEquals(Arrays.asList("type"), md.indexToName().get(2)); + assertEquals("GET /domains/{domainId}/records?name={name}&type={type} HTTP/1.1\n", md.template().toString()); } - @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "QueryParam.value\\(\\) was empty on parameter 0") - public void emptyQueryParam() throws Exception { + @Test public void emptyQueryParam() throws Exception { + thrown.expect(IllegalStateException.class); + thrown.expectMessage("QueryParam.value() was empty on parameter 0"); + contract.parseAndValidatateMetadata(WithPathAndQueryParams.class.getDeclaredMethod("emptyQueryParam", String.class)); } @@ -314,14 +332,16 @@ interface FormParams { 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")); + assertEquals(Arrays.asList("customer_name", "user_name", "password"), md.formParams()); + assertEquals(Arrays.asList("customer_name"), md.indexToName().get(0)); + assertEquals(Arrays.asList("user_name"), md.indexToName().get(1)); + assertEquals(Arrays.asList("password"), md.indexToName().get(2)); } - @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "FormParam.value\\(\\) was empty on parameter 0") - public void emptyFormParam() throws Exception { + @Test public void emptyFormParam() throws Exception { + thrown.expect(IllegalStateException.class); + thrown.expectMessage("FormParam.value() was empty on parameter 0"); + contract.parseAndValidatateMetadata(FormParams.class.getDeclaredMethod("emptyFormParam", String.class)); } @@ -334,12 +354,14 @@ interface HeaderParams { @Test public void headerParamsParseIntoIndexToName() throws Exception { MethodMetadata md = contract.parseAndValidatateMetadata(HeaderParams.class.getDeclaredMethod("logout", String.class)); - assertEquals(md.template().headers().get("Auth-Token"), ImmutableSet.of("{Auth-Token}")); - assertEquals(md.indexToName().get(0), ImmutableSet.of("Auth-Token")); + assertEquals(Arrays.asList("{Auth-Token}"), md.template().headers().get("Auth-Token")); + assertEquals(Arrays.asList("Auth-Token"), md.indexToName().get(0)); } - @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "HeaderParam.value\\(\\) was empty on parameter 0") - public void emptyHeaderParam() throws Exception { + @Test public void emptyHeaderParam() throws Exception { + thrown.expect(IllegalStateException.class); + thrown.expectMessage("HeaderParam.value() was empty on parameter 0"); + contract.parseAndValidatateMetadata(HeaderParams.class.getDeclaredMethod("emptyHeaderParam", String.class)); } @@ -350,7 +372,7 @@ interface PathsWithoutAnySlashes { @Test public void pathsWithoutSlashesParseCorrectly() throws Exception { MethodMetadata md = contract.parseAndValidatateMetadata(PathsWithoutAnySlashes.class.getDeclaredMethod("get")); - assertEquals(md.template().url(), "/base/specific"); + assertEquals("/base/specific", md.template().url()); } @Path("/base") @@ -360,7 +382,7 @@ interface PathsWithSomeSlashes { @Test public void pathsWithSomeSlashesParseCorrectly() throws Exception { MethodMetadata md = contract.parseAndValidatateMetadata(PathsWithSomeSlashes.class.getDeclaredMethod("get")); - assertEquals(md.template().url(), "/base/specific"); + assertEquals("/base/specific", md.template().url()); } @Path("base") @@ -370,6 +392,6 @@ interface PathsWithSomeOtherSlashes { @Test public void pathsWithSomeOtherSlashesParseCorrectly() throws Exception { MethodMetadata md = contract.parseAndValidatateMetadata(PathsWithSomeOtherSlashes.class.getDeclaredMethod("get")); - assertEquals(md.template().url(), "/base/specific"); + assertEquals("/base/specific", md.template().url()); } } diff --git a/ribbon/build.gradle b/ribbon/build.gradle index a01cfe0976..862cae5e06 100644 --- a/ribbon/build.gradle +++ b/ribbon/build.gradle @@ -2,13 +2,9 @@ apply plugin: 'java' sourceCompatibility = 1.6 -test { - useTestNG() -} - dependencies { compile project(':feign-core') compile 'com.netflix.ribbon:ribbon-loadbalancer:2.0-RC5' - testCompile 'org.testng:testng:6.8.5' - testCompile 'com.google.mockwebserver:mockwebserver:20130706' + testCompile 'junit:junit:4.12' + testCompile 'com.squareup.okhttp:mockwebserver:2.2.0' } diff --git a/ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java b/ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java index 70c34bc8f8..79999a8eff 100644 --- a/ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java +++ b/ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java @@ -15,38 +15,33 @@ */ package feign.ribbon; -import com.google.mockwebserver.MockResponse; -import com.google.mockwebserver.MockWebServer; - -import org.testng.annotations.Test; - -import java.io.IOException; -import java.net.URL; - +import com.squareup.okhttp.mockwebserver.MockResponse; +import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule; import feign.Feign; import feign.RequestLine; +import java.io.IOException; +import java.net.URL; +import org.junit.Rule; +import org.junit.Test; import static com.netflix.config.ConfigurationManager.getConfigInstance; import static feign.Util.UTF_8; -import static org.testng.Assert.assertEquals; +import static org.junit.Assert.assertEquals; -@Test public class LoadBalancingTargetTest { + @Rule public final MockWebServerRule server1 = new MockWebServerRule(); + @Rule public final MockWebServerRule server2 = new MockWebServerRule(); + interface TestInterface { @RequestLine("POST /") void post(); } - @Test - public void loadBalancingDefaultPolicyRoundRobin() throws IOException, InterruptedException { + @Test public void loadBalancingDefaultPolicyRoundRobin() throws IOException, InterruptedException { String name = "LoadBalancingTargetTest-loadBalancingDefaultPolicyRoundRobin"; String serverListKey = name + ".ribbon.listOfServers"; - MockWebServer server1 = new MockWebServer(); server1.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8))); - server1.play(); - MockWebServer server2 = new MockWebServer(); server2.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8))); - server2.play(); getConfigInstance().setProperty(serverListKey, hostAndPort(server1.getUrl("")) + "," + hostAndPort(server2.getUrl(""))); @@ -57,13 +52,11 @@ public void loadBalancingDefaultPolicyRoundRobin() throws IOException, Interrupt api.post(); api.post(); - assertEquals(server1.getRequestCount(), 1); - assertEquals(server2.getRequestCount(), 1); + assertEquals(1, server1.getRequestCount()); + assertEquals(1, server2.getRequestCount()); // TODO: verify ribbon stats match // assertEquals(target.lb().getLoadBalancerStats().getSingleServerStat()) } finally { - server1.shutdown(); - server2.shutdown(); getConfigInstance().clearProperty(serverListKey); } } diff --git a/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java b/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java index 42ef0e6136..d447116927 100644 --- a/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java +++ b/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java @@ -15,31 +15,32 @@ */ package feign.ribbon; -import com.google.mockwebserver.MockResponse; -import com.google.mockwebserver.MockWebServer; -import com.google.mockwebserver.SocketPolicy; +import com.squareup.okhttp.mockwebserver.MockResponse; +import com.squareup.okhttp.mockwebserver.SocketPolicy; +import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule; import dagger.Provides; -import feign.Client; import feign.Feign; import feign.RequestLine; import feign.codec.Decoder; import feign.codec.Encoder; -import org.testng.annotations.Test; import java.io.IOException; import java.net.URL; import static com.netflix.config.ConfigurationManager.getConfigInstance; -import static feign.Util.UTF_8; -import static org.testng.Assert.assertEquals; +import static org.junit.Assert.assertEquals; import javax.inject.Named; +import org.junit.Rule; +import org.junit.Test; -@Test public class RibbonClientTest { + @Rule public final MockWebServerRule server1 = new MockWebServerRule(); + @Rule public final MockWebServerRule server2 = new MockWebServerRule(); + interface TestInterface { @RequestLine("POST /") void post(); - @RequestLine("GET /?a={a}") void getWithQueryParameters(@Named("a") String a); + @RequestLine("GET /?a={a}") void getWithQueryParameters(@Named("a") String a); @dagger.Module(injects = Feign.class, overrides = true, addsTo = Feign.Defaults.class) static class Module { @@ -58,29 +59,22 @@ public void loadBalancingDefaultPolicyRoundRobin() throws IOException, Interrupt String client = "RibbonClientTest-loadBalancingDefaultPolicyRoundRobin"; String serverListKey = client + ".ribbon.listOfServers"; - MockWebServer server1 = new MockWebServer(); - server1.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8))); - server1.play(); - MockWebServer server2 = new MockWebServer(); - server2.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8))); - server2.play(); + server1.enqueue(new MockResponse().setBody("success!")); + server2.enqueue(new MockResponse().setBody("success!")); getConfigInstance().setProperty(serverListKey, hostAndPort(server1.getUrl("")) + "," + hostAndPort(server2.getUrl(""))); try { - TestInterface api = Feign.create(TestInterface.class, "http://" + client, new TestInterface.Module(), new RibbonModule()); api.post(); api.post(); - assertEquals(server1.getRequestCount(), 1); - assertEquals(server2.getRequestCount(), 1); + assertEquals(1, server1.getRequestCount()); + assertEquals(1, server2.getRequestCount()); // TODO: verify ribbon stats match // assertEquals(target.lb().getLoadBalancerStats().getSingleServerStat()) - } finally { - server1.shutdown(); - server2.shutdown(); + } finally { getConfigInstance().clearProperty(serverListKey); } } @@ -90,24 +84,20 @@ public void ioExceptionRetry() throws IOException, InterruptedException { String client = "RibbonClientTest-ioExceptionRetry"; String serverListKey = client + ".ribbon.listOfServers"; - MockWebServer server = new MockWebServer(); - server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); - server.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8))); - server.play(); + server1.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); + server1.enqueue(new MockResponse().setBody("success!")); - getConfigInstance().setProperty(serverListKey, hostAndPort(server.getUrl(""))); + getConfigInstance().setProperty(serverListKey, hostAndPort(server1.getUrl(""))); try { - TestInterface api = Feign.create(TestInterface.class, "http://" + client, new TestInterface.Module(), new RibbonModule()); api.post(); - assertEquals(server.getRequestCount(), 2); + assertEquals(2, server1.getRequestCount()); // TODO: verify ribbon stats match // assertEquals(target.lb().getLoadBalancerStats().getSingleServerStat()) } finally { - server.shutdown(); getConfigInstance().clearProperty(serverListKey); } } @@ -126,11 +116,9 @@ invalid characters (ex. space). String expectedQueryStringValue = "some+string+with+space"; String expectedRequestLine = String.format("GET /?a=%s HTTP/1.1", expectedQueryStringValue); - MockWebServer server = new MockWebServer(); - server.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8))); - server.play(); + server1.enqueue(new MockResponse().setBody("success!")); - getConfigInstance().setProperty(serverListKey, hostAndPort(server.getUrl(""))); + getConfigInstance().setProperty(serverListKey, hostAndPort(server1.getUrl(""))); try { @@ -138,11 +126,10 @@ invalid characters (ex. space). api.getWithQueryParameters(queryStringValue); - final String recordedRequestLine = server.takeRequest().getRequestLine(); + final String recordedRequestLine = server1.takeRequest().getRequestLine(); assertEquals(recordedRequestLine, expectedRequestLine); } finally { - server.shutdown(); getConfigInstance().clearProperty(serverListKey); } } @@ -153,12 +140,10 @@ public void ioExceptionRetryWithBuilder() throws IOException, InterruptedExcepti String client = "RibbonClientTest-ioExceptionRetryWithBuilder"; String serverListKey = client + ".ribbon.listOfServers"; - MockWebServer server = new MockWebServer(); - server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); - server.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8))); - server.play(); + server1.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); + server1.enqueue(new MockResponse().setBody("success!")); - getConfigInstance().setProperty(serverListKey, hostAndPort(server.getUrl(""))); + getConfigInstance().setProperty(serverListKey, hostAndPort(server1.getUrl(""))); try { @@ -168,11 +153,10 @@ public void ioExceptionRetryWithBuilder() throws IOException, InterruptedExcepti api.post(); - assertEquals(server.getRequestCount(), 2); + assertEquals(server1.getRequestCount(), 2); // TODO: verify ribbon stats match // assertEquals(target.lb().getLoadBalancerStats().getSingleServerStat()) } finally { - server.shutdown(); getConfigInstance().clearProperty(serverListKey); } } diff --git a/sax/build.gradle b/sax/build.gradle index dbb9b9a6ab..b50c180267 100644 --- a/sax/build.gradle +++ b/sax/build.gradle @@ -2,12 +2,8 @@ apply plugin: 'java' sourceCompatibility = 1.6 -test { - useTestNG() -} - dependencies { compile project(':feign-core') testCompile 'com.google.guava:guava:14.0.1' - testCompile 'org.testng:testng:6.8.5' + testCompile 'junit:junit:4.12' } diff --git a/sax/src/test/java/feign/sax/SAXDecoderTest.java b/sax/src/test/java/feign/sax/SAXDecoderTest.java index c4b9abf07c..cd3de0ec6d 100644 --- a/sax/src/test/java/feign/sax/SAXDecoderTest.java +++ b/sax/src/test/java/feign/sax/SAXDecoderTest.java @@ -19,24 +19,26 @@ import dagger.Provides; import feign.Response; import feign.codec.Decoder; -import org.testng.annotations.BeforeClass; -import org.testng.annotations.Test; -import org.xml.sax.helpers.DefaultHandler; - -import javax.inject.Inject; -import javax.inject.Provider; import java.io.IOException; import java.text.ParseException; import java.util.Collection; import java.util.Collections; - -import static org.testng.Assert.assertEquals; +import javax.inject.Inject; +import javax.inject.Provider; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.xml.sax.helpers.DefaultHandler; import static feign.Util.UTF_8; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; // unbound wildcards are not currently injectable in dagger. @SuppressWarnings("rawtypes") public class SAXDecoderTest { + @Rule public final ExpectedException thrown = ExpectedException.none(); @dagger.Module(injects = SAXDecoderTest.class) static class Module { @@ -50,18 +52,19 @@ static class Module { @Inject Decoder decoder; - @BeforeClass void inject() { + @Before public void inject() { ObjectGraph.create(new Module()).inject(this); } @Test public void parsesConfiguredTypes() throws ParseException, IOException { - assertEquals(decoder.decode(statusFailedResponse(), NetworkStatus.class), NetworkStatus.FAILED); - assertEquals(decoder.decode(statusFailedResponse(), String.class), "Failed"); + assertEquals(NetworkStatus.FAILED, decoder.decode(statusFailedResponse(), NetworkStatus.class)); + assertEquals("Failed", decoder.decode(statusFailedResponse(), String.class)); } - @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = - "type int not in configured handlers \\[class .*NetworkStatus, class java.lang.String\\]") - public void niceErrorOnUnconfiguredType() throws ParseException, IOException { + @Test public void niceErrorOnUnconfiguredType() throws ParseException, IOException { + thrown.expect(IllegalStateException.class); + thrown.expectMessage("type int not in configured handlers"); + decoder.decode(statusFailedResponse(), int.class); } @@ -140,6 +143,6 @@ public void characters(char ch[], int start, int length) { @Test public void nullBodyDecodesToNull() throws Exception { Response response = Response.create(204, "OK", Collections.>emptyMap(), null); - assertEquals(decoder.decode(response, String.class), null); + assertNull(decoder.decode(response, String.class)); } } diff --git a/slf4j/build.gradle b/slf4j/build.gradle index 7b261b02f3..144e040043 100644 --- a/slf4j/build.gradle +++ b/slf4j/build.gradle @@ -1,12 +1,8 @@ apply plugin: 'java' -test { - useTestNG() -} - dependencies { compile project(':feign-core') compile 'org.slf4j:slf4j-api:1.7.5' - testCompile 'org.testng:testng:6.8.5' + testCompile 'junit:junit:4.12' testCompile 'org.slf4j:slf4j-simple:1.7.5' } diff --git a/slf4j/src/test/java/feign/slf4j/RecordingSimpleLogger.java b/slf4j/src/test/java/feign/slf4j/RecordingSimpleLogger.java new file mode 100644 index 0000000000..9525c87e1b --- /dev/null +++ b/slf4j/src/test/java/feign/slf4j/RecordingSimpleLogger.java @@ -0,0 +1,78 @@ +/* + * 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.slf4j; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.slf4j.LoggerFactory; +import org.slf4j.impl.SimpleLogger; +import org.slf4j.impl.SimpleLoggerFactory; + +import static org.junit.Assert.assertEquals; +import static org.slf4j.impl.SimpleLogger.DEFAULT_LOG_LEVEL_KEY; +import static org.slf4j.impl.SimpleLogger.SHOW_THREAD_NAME_KEY; + +/** + * A testing utility to allow control over {@link org.slf4j.impl.SimpleLogger}. + * In some cases, reflection is used to bypass access restrictions. + */ +final class RecordingSimpleLogger implements TestRule { + + private String expectedMessages = ""; + + /** Resets {@link org.slf4j.impl.SimpleLogger} to the new log level. */ + RecordingSimpleLogger logLevel(String logLevel) throws Exception { + System.setProperty(SHOW_THREAD_NAME_KEY, "false"); + System.setProperty(DEFAULT_LOG_LEVEL_KEY, logLevel); + + Field field = SimpleLogger.class.getDeclaredField("INITIALIZED"); + field.setAccessible(true); + field.set(null, false); + + Method method = SimpleLoggerFactory.class.getDeclaredMethod("reset"); + method.setAccessible(true); + method.invoke(LoggerFactory.getILoggerFactory()); + return this; + } + + /** Newline delimited output that would be sent to stderr. */ + RecordingSimpleLogger expectMessages(String expectedMessages) { + this.expectedMessages = expectedMessages; + return this; + } + + /** Steals the output of stderr as that's where the log events go. */ + @Override public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override public void evaluate() throws Throwable { + ByteArrayOutputStream buff = new ByteArrayOutputStream(); + PrintStream stderr = System.err; + try { + System.setErr(new PrintStream(buff)); + base.evaluate(); + assertEquals(expectedMessages, buff.toString()); + } finally { + System.setErr(stderr); + } + } + }; + } +} diff --git a/slf4j/src/test/java/feign/slf4j/ReflectionUtil.java b/slf4j/src/test/java/feign/slf4j/ReflectionUtil.java deleted file mode 100644 index 2fa083bc68..0000000000 --- a/slf4j/src/test/java/feign/slf4j/ReflectionUtil.java +++ /dev/null @@ -1,37 +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.slf4j; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; - -/** - * Lightweight approach to using reflection to bypass access restrictions for testing. If this class grows, it may be - * better to use a testing library instead, such as Powermock. - */ -class ReflectionUtil { - static void setStaticField(Class declaringClass, String fieldName, Object fieldValue) throws Exception { - Field field = declaringClass.getDeclaredField(fieldName); - field.setAccessible(true); - field.set(null, fieldValue); - } - - static void invokeVoidNoArgMethod(Class declaringClass, String methodName, Object instance) throws Exception { - Method method = declaringClass.getDeclaredMethod(methodName); - method.setAccessible(true); - method.invoke(instance); - } -} diff --git a/slf4j/src/test/java/feign/slf4j/SimpleLoggerUtil.java b/slf4j/src/test/java/feign/slf4j/SimpleLoggerUtil.java deleted file mode 100644 index e676e1470e..0000000000 --- a/slf4j/src/test/java/feign/slf4j/SimpleLoggerUtil.java +++ /dev/null @@ -1,47 +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.slf4j; - -import org.slf4j.LoggerFactory; -import org.slf4j.impl.SimpleLogger; -import org.slf4j.impl.SimpleLoggerFactory; - -import java.io.File; - -/** - * A testing utility to allow control over {@link SimpleLogger}. In some cases, reflection is used to bypass access - * restrictions. - */ -class SimpleLoggerUtil { - static void initialize(File file, String logLevel) throws Exception { - System.setProperty(SimpleLogger.SHOW_THREAD_NAME_KEY, "false"); - System.setProperty(SimpleLogger.LOG_FILE_KEY, file.getAbsolutePath()); - System.setProperty(SimpleLogger.DEFAULT_LOG_LEVEL_KEY, logLevel); - resetSlf4j(); - } - - static void resetToDefaults() throws Exception { - System.clearProperty(SimpleLogger.SHOW_THREAD_NAME_KEY); - System.clearProperty(SimpleLogger.LOG_FILE_KEY); - System.clearProperty(SimpleLogger.DEFAULT_LOG_LEVEL_KEY); - resetSlf4j(); - } - - private static void resetSlf4j() throws Exception { - ReflectionUtil.setStaticField(SimpleLogger.class, "INITIALIZED", false); - ReflectionUtil.invokeVoidNoArgMethod(SimpleLoggerFactory.class, "reset", LoggerFactory.getILoggerFactory()); - } -} diff --git a/slf4j/src/test/java/feign/slf4j/Slf4jLoggerTest.java b/slf4j/src/test/java/feign/slf4j/Slf4jLoggerTest.java index 8b4ec16f2c..b81560bd4c 100644 --- a/slf4j/src/test/java/feign/slf4j/Slf4jLoggerTest.java +++ b/slf4j/src/test/java/feign/slf4j/Slf4jLoggerTest.java @@ -20,90 +20,73 @@ import feign.Request; import feign.RequestTemplate; import feign.Response; -import feign.Util; -import org.slf4j.LoggerFactory; -import org.testng.annotations.AfterMethod; -import org.testng.annotations.Test; - -import java.io.File; -import java.io.FileReader; import java.util.Collection; import java.util.Collections; - -import static org.testng.Assert.assertEquals; +import org.junit.Rule; +import org.junit.Test; +import org.slf4j.LoggerFactory; public class Slf4jLoggerTest { + @Rule public final RecordingSimpleLogger slf4j = new RecordingSimpleLogger(); + private static final String CONFIG_KEY = "someMethod()"; private static final Request REQUEST = new RequestTemplate().method("GET").append("http://api.example.com").request(); private static final Response RESPONSE = Response.create(200, "OK", Collections.>emptyMap(), new byte[0]); - private File logFile; private Slf4jLogger logger; - @AfterMethod - void tearDown() throws Exception { - SimpleLoggerUtil.resetToDefaults(); - logFile.delete(); - } - @Test public void useFeignLoggerByDefault() throws Exception { - initializeSimpleLogger("debug"); + slf4j.logLevel("debug"); + slf4j.expectMessages("DEBUG feign.Logger - [someMethod] This is my message\n"); + logger = new Slf4jLogger(); logger.log(CONFIG_KEY, "This is my message"); - assertLoggedMessages("DEBUG feign.Logger - [someMethod] This is my message\n"); } @Test public void useLoggerByNameIfRequested() throws Exception { - initializeSimpleLogger("debug"); + slf4j.logLevel("debug"); + slf4j.expectMessages("DEBUG named.logger - [someMethod] This is my message\n"); + logger = new Slf4jLogger("named.logger"); logger.log(CONFIG_KEY, "This is my message"); - assertLoggedMessages("DEBUG named.logger - [someMethod] This is my message\n"); } @Test public void useLoggerByClassIfRequested() throws Exception { - initializeSimpleLogger("debug"); + slf4j.logLevel("debug"); + slf4j.expectMessages("DEBUG feign.Feign - [someMethod] This is my message\n"); + logger = new Slf4jLogger(Feign.class); logger.log(CONFIG_KEY, "This is my message"); - assertLoggedMessages("DEBUG feign.Feign - [someMethod] This is my message\n"); } @Test public void useSpecifiedLoggerIfRequested() throws Exception { - initializeSimpleLogger("debug"); + slf4j.logLevel("debug"); + slf4j.expectMessages("DEBUG specified.logger - [someMethod] This is my message\n"); + logger = new Slf4jLogger(LoggerFactory.getLogger("specified.logger")); logger.log(CONFIG_KEY, "This is my message"); - assertLoggedMessages("DEBUG specified.logger - [someMethod] This is my message\n"); } @Test public void logOnlyIfDebugEnabled() throws Exception { - initializeSimpleLogger("info"); + slf4j.logLevel("info"); + logger = new Slf4jLogger(); logger.log(CONFIG_KEY, "A message with %d formatting %s.", 2, "tokens"); logger.logRequest(CONFIG_KEY, Logger.Level.BASIC, REQUEST); logger.logAndRebufferResponse(CONFIG_KEY, Logger.Level.BASIC, RESPONSE, 273); - assertLoggedMessages(""); } @Test public void logRequestsAndResponses() throws Exception { - initializeSimpleLogger("debug"); + slf4j.logLevel("debug"); + slf4j.expectMessages("DEBUG feign.Logger - [someMethod] A message with 2 formatting tokens.\n" + + "DEBUG feign.Logger - [someMethod] ---> GET http://api.example.com HTTP/1.1\n" + + "DEBUG feign.Logger - [someMethod] <--- HTTP/1.1 200 OK (273ms)\n"); + logger = new Slf4jLogger(); logger.log(CONFIG_KEY, "A message with %d formatting %s.", 2, "tokens"); logger.logRequest(CONFIG_KEY, Logger.Level.BASIC, REQUEST); logger.logAndRebufferResponse(CONFIG_KEY, Logger.Level.BASIC, RESPONSE, 273); - assertLoggedMessages( - "DEBUG feign.Logger - [someMethod] A message with 2 formatting tokens.\n" + - "DEBUG feign.Logger - [someMethod] ---> GET http://api.example.com HTTP/1.1\n" + - "DEBUG feign.Logger - [someMethod] <--- HTTP/1.1 200 OK (273ms)\n" - ); - } - - private void initializeSimpleLogger(String logLevel) throws Exception { - logFile = File.createTempFile(getClass().getName(), ".log"); - SimpleLoggerUtil.initialize(logFile, logLevel); - } - - private void assertLoggedMessages(String expectedMessages) throws Exception { - assertEquals(Util.toString(new FileReader(logFile)), expectedMessages); } } From 3fc385a112e2df07344dc1e51b6ba89e2970d278 Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Sun, 25 Jan 2015 17:48:00 -0800 Subject: [PATCH 155/179] Removes guava test dependency in favor of AssertJ AssertJ has more powerful test assertions and does not run the risk of interfering with the classpath of main code, such as guava does. This removes guava from test and example code and adjusts using AssertJ in some cases. --- core/build.gradle | 2 +- .../java/feign/AcceptAllHostnameVerifier.java | 26 -- .../test/java/feign/DefaultContractTest.java | 215 ++++++++-------- .../src/test/java/feign/FeignBuilderTest.java | 22 +- core/src/test/java/feign/FeignTest.java | 116 +++++---- core/src/test/java/feign/GZIPStreams.java | 41 --- core/src/test/java/feign/LoggerTest.java | 18 +- .../test/java/feign/RequestTemplateTest.java | 200 +++++++-------- .../java/feign/TrustingSSLSocketFactory.java | 38 +-- .../java/feign/assertj/FeignAssertions.java | 10 + .../assertj/MockWebServerAssertions.java | 10 + .../feign/assertj/RecordedRequestAssert.java | 87 +++++++ .../feign/assertj/RequestTemplateAssert.java | 67 +++++ .../auth/BasicAuthRequestInterceptorTest.java | 32 +-- .../feign/codec/DefaultErrorDecoderTest.java | 19 +- gson/build.gradle | 2 + .../test/java/feign/gson/GsonModuleTest.java | 30 +-- jackson/build.gradle | 3 +- .../java/feign/jackson/JacksonModuleTest.java | 17 +- jaxb/build.gradle | 3 +- .../test/java/feign/jaxb/JAXBModuleTest.java | 78 +++--- .../jaxb/examples/AWSSignatureVersion4.java | 85 +++---- .../java/feign/jaxb/examples/IAMExample.java | 108 +------- jaxrs/build.gradle | 5 +- .../java/feign/jaxrs/JAXRSContractTest.java | 240 +++++++++--------- ribbon/build.gradle | 1 + .../main/java/feign/ribbon/RibbonClient.java | 3 +- .../java/feign/ribbon/RibbonClientTest.java | 117 ++++----- sax/build.gradle | 2 +- .../sax/examples/AWSSignatureVersion4.java | 83 +++--- slf4j/build.gradle | 1 + 31 files changed, 820 insertions(+), 861 deletions(-) delete mode 100644 core/src/test/java/feign/AcceptAllHostnameVerifier.java delete mode 100644 core/src/test/java/feign/GZIPStreams.java create mode 100644 core/src/test/java/feign/assertj/FeignAssertions.java create mode 100644 core/src/test/java/feign/assertj/MockWebServerAssertions.java create mode 100644 core/src/test/java/feign/assertj/RecordedRequestAssert.java create mode 100644 core/src/test/java/feign/assertj/RequestTemplateAssert.java diff --git a/core/build.gradle b/core/build.gradle index e2ee72b06c..9edfdcb787 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -3,9 +3,9 @@ apply plugin: 'java' sourceCompatibility = 1.6 dependencies { - 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 'junit:junit:4.12' + testCompile 'org.assertj:assertj-core:1.7.1' testCompile 'com.squareup.okhttp:mockwebserver:2.2.0' } diff --git a/core/src/test/java/feign/AcceptAllHostnameVerifier.java b/core/src/test/java/feign/AcceptAllHostnameVerifier.java deleted file mode 100644 index fa0055dba3..0000000000 --- a/core/src/test/java/feign/AcceptAllHostnameVerifier.java +++ /dev/null @@ -1,26 +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 javax.net.ssl.HostnameVerifier; -import javax.net.ssl.SSLSession; - -final class AcceptAllHostnameVerifier implements HostnameVerifier { - @Override - public boolean verify(String s, SSLSession sslSession) { - return true; - } -} diff --git a/core/src/test/java/feign/DefaultContractTest.java b/core/src/test/java/feign/DefaultContractTest.java index 77174c2517..404f9f5533 100644 --- a/core/src/test/java/feign/DefaultContractTest.java +++ b/core/src/test/java/feign/DefaultContractTest.java @@ -15,21 +15,17 @@ */ package feign; -import com.google.common.collect.ImmutableList; import com.google.gson.reflect.TypeToken; import java.net.URI; -import java.util.Arrays; import java.util.List; import javax.inject.Named; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import static feign.Util.UTF_8; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; +import static feign.assertj.FeignAssertions.assertThat; +import static java.util.Arrays.asList; +import static org.assertj.core.data.MapEntry.entry; /** * Tests interfaces defined per {@link Contract.Default} are interpreted into expected {@link feign @@ -52,14 +48,17 @@ interface Methods { } @Test public void httpMethods() throws Exception { - assertEquals("POST", - contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("post")).template().method()); - assertEquals("PUT", - contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("put")).template().method()); - assertEquals("GET", - contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("get")).template().method()); - assertEquals("DELETE", - contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("delete")).template().method()); + assertThat(contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("post")).template()) + .hasMethod("POST"); + + assertThat(contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("put")).template()) + .hasMethod("PUT"); + + assertThat(contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("get")).template()) + .hasMethod("GET"); + + assertThat(contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("delete")).template()) + .hasMethod("DELETE"); } interface BodyParams { @@ -69,14 +68,12 @@ interface BodyParams { } @Test public void bodyParamIsGeneric() throws Exception { - MethodMetadata md = contract.parseAndValidatateMetadata(BodyParams.class.getDeclaredMethod("post", - List.class)); - assertNull(md.template().body()); - assertNull(md.template().bodyTemplate()); - assertNull(md.urlIndex()); - assertEquals(md.bodyIndex(), Integer.valueOf(0)); - assertEquals(md.bodyType(), new TypeToken>() { - }.getType()); + MethodMetadata md = contract.parseAndValidatateMetadata(BodyParams.class.getDeclaredMethod("post", List.class)); + + assertThat(md.bodyIndex()) + .isEqualTo(0); + assertThat(md.bodyType()) + .isEqualTo(new TypeToken>(){}.getType()); } @Test public void tooManyBodies() throws Exception { @@ -86,20 +83,14 @@ interface BodyParams { BodyParams.class.getDeclaredMethod("tooMany", List.class, List.class)); } - interface CustomMethodAndURIParam { - @RequestLine("PATCH") Response patch(URI nextLink); + interface CustomMethod { + @RequestLine("PATCH") Response patch(); } - @Test public void requestLineOnlyRequiresMethod() throws Exception { - MethodMetadata md = contract.parseAndValidatateMetadata(CustomMethodAndURIParam.class.getDeclaredMethod("patch", - URI.class)); - assertEquals("PATCH", md.template().method()); - assertEquals("", md.template().url()); - assertTrue(md.template().queries().isEmpty()); - assertTrue(md.template().headers().isEmpty()); - assertNull(md.template().body()); - assertNull(md.template().bodyTemplate()); - assertEquals(Integer.valueOf(0), md.urlIndex()); + @Test public void customMethodWithoutPath() throws Exception { + assertThat(contract.parseAndValidatateMetadata(CustomMethod.class.getDeclaredMethod("patch")).template()) + .hasMethod("PATCH") + .hasUrl(""); } interface WithQueryParamsInPath { @@ -115,41 +106,38 @@ interface WithQueryParamsInPath { } @Test public void queryParamsInPathExtract() throws Exception { - { - MethodMetadata md = contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("none")); - assertEquals("/", md.template().url()); - assertTrue(md.template().queries().isEmpty()); - assertEquals("GET / HTTP/1.1\n", md.template().toString()); - } - { - MethodMetadata md = contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("one")); - assertEquals("/", md.template().url()); - assertEquals(Arrays.asList("GetUser"), md.template().queries().get("Action")); - assertEquals("GET /?Action=GetUser HTTP/1.1\n", md.template().toString()); - } - { - MethodMetadata md = contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("two")); - assertEquals("/", md.template().url()); - assertEquals(Arrays.asList("GetUser"), md.template().queries().get("Action")); - assertEquals(Arrays.asList("2010-05-08"), md.template().queries().get("Version")); - assertEquals("GET /?Action=GetUser&Version=2010-05-08 HTTP/1.1\n", md.template().toString()); - } - { - MethodMetadata md = contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("three")); - assertEquals("/", md.template().url()); - assertEquals(Arrays.asList("GetUser"), md.template().queries().get("Action")); - assertEquals(Arrays.asList("2010-05-08"), md.template().queries().get("Version")); - assertEquals(Arrays.asList("1"), md.template().queries().get("limit")); - assertEquals("GET /?Action=GetUser&Version=2010-05-08&limit=1 HTTP/1.1\n", md.template().toString()); - } - { - MethodMetadata md = contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("empty")); - assertEquals("/", md.template().url()); - assertTrue(md.template().queries().containsKey("flag")); - assertEquals(Arrays.asList("GetUser"), md.template().queries().get("Action")); - assertEquals(Arrays.asList("2010-05-08"), md.template().queries().get("Version")); - assertEquals("GET /?flag&Action=GetUser&Version=2010-05-08 HTTP/1.1\n", md.template().toString()); - } + assertThat(contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("none")).template()) + .hasUrl("/") + .hasQueries(); + + assertThat(contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("one")).template()) + .hasUrl("/") + .hasQueries( + entry("Action", asList("GetUser")) + ); + + assertThat(contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("two")).template()) + .hasUrl("/") + .hasQueries( + entry("Action", asList("GetUser")), + entry("Version", asList("2010-05-08")) + ); + + assertThat(contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("three")).template()) + .hasUrl("/") + .hasQueries( + entry("Action", asList("GetUser")), + entry("Version", asList("2010-05-08")), + entry("limit", asList("1")) + ); + + assertThat(contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("empty")).template()) + .hasUrl("/") + .hasQueries( + entry("flag", asList(new String[] { null })), + entry("Action", asList("GetUser")), + entry("Version", asList("2010-05-08")) + ); } interface BodyWithoutParameters { @@ -160,33 +148,37 @@ interface BodyWithoutParameters { @Test public void bodyWithoutParameters() throws Exception { MethodMetadata md = contract.parseAndValidatateMetadata(BodyWithoutParameters.class.getDeclaredMethod("post")); - assertEquals("", new String(md.template().body(), UTF_8)); - assertFalse(md.template().bodyTemplate() != null); - assertTrue(md.formParams().isEmpty()); - assertTrue(md.indexToName().isEmpty()); + + assertThat(md.template()) + .hasBody(""); } @Test public void producesAddsContentTypeHeader() throws Exception { MethodMetadata md = contract.parseAndValidatateMetadata(BodyWithoutParameters.class.getDeclaredMethod("post")); - assertEquals(Arrays.asList("application/xml"), md.template().headers().get("Content-Type")); + + assertThat(md.template()) + .hasHeaders( + entry("Content-Type", asList("application/xml")), + entry("Content-Length", asList(String.valueOf(md.template().body().length))) + ); } interface WithURIParam { @RequestLine("GET /{1}/{2}") Response uriParam(@Named("1") String one, URI endpoint, @Named("2") String two); } - @Test public void methodCanHaveUriParam() throws Exception { - MethodMetadata md = contract.parseAndValidatateMetadata(WithURIParam.class.getDeclaredMethod("uriParam", String.class, - URI.class, String.class)); - assertEquals(Integer.valueOf(1), md.urlIndex()); - } + @Test public void withPathAndURIParam() throws Exception { + MethodMetadata md = contract.parseAndValidatateMetadata( + WithURIParam.class.getDeclaredMethod("uriParam", String.class, URI.class, String.class)); - @Test public void pathParamsParseIntoIndexToName() throws Exception { - MethodMetadata md = contract.parseAndValidatateMetadata(WithURIParam.class.getDeclaredMethod("uriParam", String.class, - URI.class, String.class)); - assertEquals("/{1}/{2}", md.template().url()); - assertEquals(Arrays.asList("1"), md.indexToName().get(0)); - assertEquals(Arrays.asList("2"), md.indexToName().get(2)); + assertThat(md.indexToName()) + .containsExactly( + entry(0, asList("1")), + // Skips 1 as it is a url index! + entry(2, asList("2")) + ); + + assertThat(md.urlIndex()).isEqualTo(1); } interface WithPathAndQueryParams { @@ -195,19 +187,18 @@ Response recordsByNameAndType(@Named("domainId") int id, @Named("name") String n @Named("type") String typeFilter); } - @Test public void mixedRequestLineParams() throws Exception { + @Test public void pathAndQueryParams() throws Exception { MethodMetadata md = contract.parseAndValidatateMetadata(WithPathAndQueryParams.class.getDeclaredMethod ("recordsByNameAndType", int.class, String.class, String.class)); - assertNull(md.template().body()); - assertNull(md.template().bodyTemplate()); - assertTrue(md.template().headers().isEmpty()); - assertEquals("/domains/{domainId}/records", md.template().url()); - assertEquals(Arrays.asList("{name}"), md.template().queries().get("name")); - assertEquals(Arrays.asList("{type}"), md.template().queries().get("type")); - assertEquals(Arrays.asList("domainId"), md.indexToName().get(0)); - assertEquals(Arrays.asList("name"), md.indexToName().get(1)); - assertEquals(Arrays.asList("type"), md.indexToName().get(2)); - assertEquals("GET /domains/{domainId}/records?name={name}&type={type} HTTP/1.1\n", md.template().toString()); + + assertThat(md.template()) + .hasQueries(entry("name", asList("{name}")), entry("type", asList("{type}"))); + + assertThat(md.indexToName()).containsExactly( + entry(0, asList("domainId")), + entry(1, asList("name")), + entry(2, asList("type")) + ); } interface FormParams { @@ -218,18 +209,26 @@ void login( @Named("user_name") String user, @Named("password") String password); } + @Test public void bodyWithTemplate() throws Exception { + MethodMetadata md = contract.parseAndValidatateMetadata(FormParams.class.getDeclaredMethod("login", String.class, + String.class, String.class)); + + assertThat(md.template()) + .hasBodyTemplate("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D"); + } + @Test public void formParamsParseIntoIndexToName() throws Exception { MethodMetadata md = contract.parseAndValidatateMetadata(FormParams.class.getDeclaredMethod("login", String.class, String.class, String.class)); - assertFalse(md.template().body() != null); - assertEquals( - "%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D", - md.template().bodyTemplate()); - assertEquals(ImmutableList.of("customer_name", "user_name", "password"), md.formParams()); - assertEquals(Arrays.asList("customer_name"), md.indexToName().get(0)); - assertEquals(Arrays.asList("user_name"), md.indexToName().get(1)); - assertEquals(Arrays.asList("password"), md.indexToName().get(2)); + assertThat(md.formParams()) + .containsExactly("customer_name", "user_name", "password"); + + assertThat(md.indexToName()).containsExactly( + entry(0, asList("customer_name")), + entry(1, asList("user_name")), + entry(2, asList("password")) + ); } interface HeaderParams { @@ -240,7 +239,9 @@ interface HeaderParams { @Test public void headerParamsParseIntoIndexToName() throws Exception { MethodMetadata md = contract.parseAndValidatateMetadata(HeaderParams.class.getDeclaredMethod("logout", String.class)); - assertEquals(Arrays.asList("{Auth-Token}"), md.template().headers().get("Auth-Token")); - assertEquals(Arrays.asList("Auth-Token"), md.indexToName().get(0)); + assertThat(md.template()).hasHeaders(entry("Auth-Token", asList("{Auth-Token}"))); + + assertThat(md.indexToName()) + .containsExactly(entry(0, asList("Auth-Token"))); } } diff --git a/core/src/test/java/feign/FeignBuilderTest.java b/core/src/test/java/feign/FeignBuilderTest.java index 1ab2f58333..5020e2b2b1 100644 --- a/core/src/test/java/feign/FeignBuilderTest.java +++ b/core/src/test/java/feign/FeignBuilderTest.java @@ -16,7 +16,6 @@ package feign; import com.squareup.okhttp.mockwebserver.MockResponse; -import com.squareup.okhttp.mockwebserver.RecordedRequest; import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule; import feign.codec.Decoder; import feign.codec.EncodeException; @@ -32,6 +31,7 @@ import java.util.concurrent.atomic.AtomicInteger; import org.junit.Test; +import static feign.assertj.MockWebServerAssertions.assertThat; import static org.junit.Assert.assertEquals; public class FeignBuilderTest { @@ -54,8 +54,8 @@ interface TestInterface { Response response = api.codecPost("request data"); assertEquals("response data", Util.toString(response.body().asReader())); - assertEquals(1, server.getRequestCount()); - assertEquals("request data", server.takeRequest().getUtf8Body()); + assertThat(server.takeRequest()) + .hasBody("request data"); } @Test public void testOverrideEncoder() throws Exception { @@ -72,8 +72,8 @@ public void encode(Object object, RequestTemplate template) throws EncodeExcepti TestInterface api = Feign.builder().encoder(encoder).target(TestInterface.class, url); api.encodedPost(Arrays.asList("This", "is", "my", "request")); - assertEquals(1, server.getRequestCount()); - assertEquals("[This, is, my, request]", server.takeRequest().getUtf8Body()); + assertThat(server.takeRequest()) + .hasBody("[This, is, my, request]"); } @Test public void testOverrideDecoder() throws Exception { @@ -108,10 +108,9 @@ public void apply(RequestTemplate template) { Response response = api.codecPost("request data"); assertEquals(Util.toString(response.body().asReader()), "response data"); - assertEquals(1, server.getRequestCount()); - RecordedRequest request = server.takeRequest(); - assertEquals("request data", request.getUtf8Body()); - assertEquals("text/plain", request.getHeader("Content-Type")); + assertThat(server.takeRequest()) + .hasHeaders("Content-Type: text/plain") + .hasBody("request data"); } @Test public void testProvideInvocationHandlerFactory() throws Exception { @@ -133,8 +132,7 @@ public void apply(RequestTemplate template) { assertEquals("response data", Util.toString(response.body().asReader())); assertEquals(1, callCount.get()); - assertEquals(1, server.getRequestCount()); - RecordedRequest request = server.takeRequest(); - assertEquals("request data", request.getUtf8Body()); + assertThat(server.takeRequest()) + .hasBody("request data"); } } diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index f63a122581..b409ea9093 100644 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -15,12 +15,9 @@ */ package feign; -import com.google.common.base.Joiner; -import com.google.common.io.ByteStreams; -import com.google.common.io.CharStreams; +import com.google.gson.Gson; import com.squareup.okhttp.mockwebserver.MockResponse; import com.squareup.okhttp.mockwebserver.MockWebServer; -import com.squareup.okhttp.mockwebserver.RecordedRequest; import com.squareup.okhttp.mockwebserver.SocketPolicy; import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule; import dagger.Module; @@ -40,6 +37,7 @@ import javax.inject.Named; import javax.inject.Singleton; import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocketFactory; import org.junit.Rule; import org.junit.Test; @@ -47,10 +45,8 @@ import static dagger.Provides.Type.SET; import static feign.Util.UTF_8; -import static org.junit.Assert.assertArrayEquals; +import static feign.assertj.MockWebServerAssertions.assertThat; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; // unbound wildcards are not currently injectable in dagger. @@ -90,7 +86,7 @@ static class Module { return new Encoder() { @Override public void encode(Object object, RequestTemplate template) { if (object instanceof Map) { - template.body(Joiner.on(',').withKeyValueSeparator("=").join((Map) object)); + template.body(new Gson().toJson(object)); } else { template.body(object.toString()); } @@ -100,14 +96,16 @@ static class Module { } } - @Test - public void iterableQueryParams() throws IOException, InterruptedException { + @Test public void iterableQueryParams() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("foo")); - TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module()); + TestInterface api = + Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module()); api.queryParams("user", Arrays.asList("apple", "pear")); - assertEquals("GET /?1=user&2=apple&2=pear HTTP/1.1", server.takeRequest().getRequestLine()); + + assertThat(server.takeRequest()) + .hasPath("/?1=user&2=apple&2=pear"); } interface OtherTestInterface { @@ -136,8 +134,9 @@ public void postTemplateParamsResolve() throws IOException, InterruptedException TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module()); api.login("netflix", "denominator", "password"); - assertEquals("{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}", - server.takeRequest().getUtf8Body()); + + assertThat(server.takeRequest()) + .hasBody("{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}"); } @Test @@ -159,8 +158,9 @@ public void postFormParams() throws IOException, InterruptedException { TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module()); api.form("netflix", "denominator", "password"); - assertEquals("customer_name=netflix,user_name=denominator,password=password", - server.takeRequest().getUtf8Body()); + + assertThat(server.takeRequest()) + .hasBody("{\"customer_name\":\"netflix\",\"user_name\":\"denominator\",\"password\":\"password\"}"); } @Test @@ -170,9 +170,10 @@ public void postBodyParam() throws IOException, InterruptedException { TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module()); api.body(Arrays.asList("netflix", "denominator", "password")); - RecordedRequest request = server.takeRequest(); - assertEquals("32", request.getHeader("Content-Length")); - assertEquals("[netflix, denominator, password]", request.getUtf8Body()); + + assertThat(server.takeRequest()) + .hasHeaders("Content-Length: 32") + .hasBody("[netflix, denominator, password]"); } @Test @@ -182,12 +183,10 @@ public void postGZIPEncodedBodyParam() throws IOException, InterruptedException 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("[netflix, denominator, password]", uncompressedBody); + + assertThat(server.takeRequest()) + .hasNoHeaderNamed("Content-Length") + .hasGzippedBody("[netflix, denominator, password]".getBytes(UTF_8)); } @Module(library = true) @@ -209,7 +208,9 @@ public void singleInterceptor() throws IOException, InterruptedException { new TestInterface.Module(), new ForwardedForInterceptor()); api.post(); - assertEquals("origin.host.com", server.takeRequest().getHeader("X-Forwarded-For")); + + assertThat(server.takeRequest()) + .hasHeaders("X-Forwarded-For: origin.host.com"); } @Module(library = true) @@ -231,9 +232,9 @@ public void multipleInterceptor() throws IOException, InterruptedException { new TestInterface.Module(), new ForwardedForInterceptor(), new UserAgentInterceptor()); api.post(); - RecordedRequest request = server.takeRequest(); - assertEquals("origin.host.com", request.getHeader("X-Forwarded-For")); - assertEquals("Feign", request.getHeader("User-Agent")); + + assertThat(server.takeRequest()) + .hasHeaders("X-Forwarded-For: origin.host.com", "User-Agent: Feign"); } @Test public void toKeyMethodFormatsAsExpected() throws Exception { @@ -278,6 +279,7 @@ public void canOverrideErrorDecoder() throws IOException, InterruptedException { new TestInterface.Module()); api.post(); + assertEquals(2, server.getRequestCount()); } @@ -293,14 +295,13 @@ public Object decode(Response response, Type type) { } } - public void overrideTypeSpecificDecoder() throws IOException, InterruptedException { + @Test public void overrideTypeSpecificDecoder() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("success!")); TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new DecodeFail()); assertEquals(api.post(), "fail"); - assertEquals(1, server.getRequestCount()); } @dagger.Module(overrides = true, library = true, includes = TestInterface.Module.class) @@ -385,7 +386,12 @@ static class TrustSSLSockets { @Module(overrides = true, includes = TrustSSLSockets.class) static class DisableHostnameVerification { @Provides HostnameVerifier acceptAllHostnameVerifier() { - return new AcceptAllHostnameVerifier(); + return new HostnameVerifier() { + @Override + public boolean verify(String s, SSLSession sslSession) { + return true; + } + }; } } @@ -431,28 +437,29 @@ static class DisableHostnameVerification { TestInterface i3 = Feign.builder().target(t2); OtherTestInterface i4 = Feign.builder().target(t3); - assertEquals(i1, i1); - assertEquals(i2, i1); - assertNotEquals(i3, i1); - assertNotEquals(i4, i1); + assertThat(i1) + .isEqualTo(i2) + .isNotEqualTo(i3) + .isNotEqualTo(i4); + + assertThat(i1.hashCode()) + .isEqualTo(i2.hashCode()) + .isNotEqualTo(i3.hashCode()) + .isNotEqualTo(i4.hashCode()); - assertEquals(i1.hashCode(), i1.hashCode()); - assertEquals(i2.hashCode(), i1.hashCode()); - assertNotEquals(i3.hashCode(), i1.hashCode()); - assertNotEquals(i4.hashCode(), i1.hashCode()); + assertThat(i1.toString()) + .isEqualTo(i2.toString()) + .isNotEqualTo(i3.toString()) + .isNotEqualTo(i4.toString()); - assertEquals(t1.hashCode(), i1.hashCode()); - assertEquals(t2.hashCode(), i3.hashCode()); - assertEquals(t3.hashCode(), i4.hashCode()); + assertThat(t1) + .isNotEqualTo(i1); - assertEquals(i1.toString(), i1.toString()); - assertEquals(i2.toString(), i1.toString()); - assertNotEquals(i3.toString(), i1.toString()); - assertNotEquals(i4.toString(), i1.toString()); + assertThat(t1.hashCode()) + .isEqualTo(i1.hashCode()); - assertEquals(t1.toString(), i1.toString()); - assertEquals(t2.toString(), i3.toString()); - assertEquals(t3.toString(), i4.toString()); + assertThat(t1.toString()) + .isEqualTo(i1.toString()); } @Test public void decodeLogicSupportsByteArray() throws Exception { @@ -461,8 +468,8 @@ static class DisableHostnameVerification { OtherTestInterface api = Feign.builder().target(OtherTestInterface.class, "http://localhost:" + server.getPort()); - byte[] actualResponse = api.binaryResponseBody(); - assertArrayEquals(expectedResponse, actualResponse); + assertThat(api.binaryResponseBody()) + .containsExactly(expectedResponse); } @Test public void encodeLogicSupportsByteArray() throws Exception { @@ -472,7 +479,8 @@ static class DisableHostnameVerification { OtherTestInterface api = Feign.builder().target(OtherTestInterface.class, "http://localhost:" + server.getPort()); api.binaryRequestBody(expectedRequest); - byte[] actualRequest = server.takeRequest().getBody(); - assertArrayEquals(expectedRequest, actualRequest); + + assertThat(server.takeRequest()) + .hasBody(expectedRequest); } } diff --git a/core/src/test/java/feign/GZIPStreams.java b/core/src/test/java/feign/GZIPStreams.java deleted file mode 100644 index 42b2886825..0000000000 --- a/core/src/test/java/feign/GZIPStreams.java +++ /dev/null @@ -1,41 +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 com.google.common.io.InputSupplier; - -import java.io.IOException; -import java.io.InputStream; -import java.util.zip.GZIPInputStream; - -class GZIPStreams { - static InputSupplier newInputStreamSupplier(InputSupplier supplier) { - return new GZIPInputStreamSupplier(supplier); - } - - private static class GZIPInputStreamSupplier implements InputSupplier { - private final InputSupplier supplier; - - GZIPInputStreamSupplier(InputSupplier supplier) { - this.supplier = supplier; - } - - @Override - public GZIPInputStream getInput() throws IOException { - return new GZIPInputStream(supplier.getInput()); - } - } -} diff --git a/core/src/test/java/feign/LoggerTest.java b/core/src/test/java/feign/LoggerTest.java index 5e2001152d..57cc9ca1ae 100644 --- a/core/src/test/java/feign/LoggerTest.java +++ b/core/src/test/java/feign/LoggerTest.java @@ -15,7 +15,6 @@ */ package feign; -import com.google.common.base.Joiner; import com.squareup.okhttp.mockwebserver.MockResponse; import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule; import feign.Logger.Level; @@ -24,8 +23,8 @@ import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; -import java.util.regex.Pattern; import javax.inject.Named; +import org.assertj.core.api.SoftAssertions; import org.junit.Rule; import org.junit.Test; import org.junit.experimental.runners.Enclosed; @@ -37,10 +36,6 @@ import org.junit.runners.Parameterized.Parameters; import org.junit.runners.model.Statement; -import static feign.Util.UTF_8; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - @RunWith(Enclosed.class) public class LoggerTest { @Rule public final MockWebServerRule server = new MockWebServerRule(); @@ -104,9 +99,6 @@ public LogLevelEmitsTest(Level logLevel, List expectedMessages) { .target(SendsStuff.class, "http://localhost:" + server.getUrl("").getPort()); api.login("netflix", "denominator", "password"); - - assertEquals(new String(server.takeRequest().getBody(), UTF_8), - "{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}"); } } @@ -247,7 +239,7 @@ public RetryEmitsTest(Level logLevel, List expectedMessages) { SendsStuff api = Feign.builder() .logger(logger) .logLevel(logLevel) - .retryer( new Retryer() { + .retryer(new Retryer() { boolean retried; @Override public void continueOrPropagate(RetryableException e) { @@ -281,11 +273,11 @@ RecordingLogger expectMessages(List expectedMessages){ return new Statement() { @Override public void evaluate() throws Throwable { base.evaluate(); - assertEquals(messages.size(), expectedMessages.size()); + SoftAssertions softly = new SoftAssertions(); for (int i = 0; i < messages.size(); i++) { - assertTrue("Didn't match at message " + (i + 1) + ":\n" + Joiner.on('\n').join(messages), - Pattern.compile(expectedMessages.get(i), Pattern.DOTALL).matcher(messages.get(i)).matches()); + softly.assertThat(messages.get(i)).matches(expectedMessages.get(i)); } + softly.assertAll(); } }; } diff --git a/core/src/test/java/feign/RequestTemplateTest.java b/core/src/test/java/feign/RequestTemplateTest.java index 6c873a048e..032c82c6eb 100644 --- a/core/src/test/java/feign/RequestTemplateTest.java +++ b/core/src/test/java/feign/RequestTemplateTest.java @@ -15,89 +15,84 @@ */ package feign; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableListMultimap; -import com.google.common.collect.ImmutableMap; - import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; import org.junit.Test; +import static feign.assertj.FeignAssertions.assertThat; import static feign.RequestTemplate.expand; -import static org.junit.Assert.assertEquals; +import static java.util.Arrays.asList; +import static org.assertj.core.data.MapEntry.entry; public class RequestTemplateTest { + @Test public void expandNotUrlEncoded() { - for (String val : ImmutableList.of("apples", "sp ace", "unic???de", "qu?stion")) - assertEquals("/users/" + val, expand("/users/{user}", ImmutableMap.of("user", val))); + for (String val : Arrays.asList("apples", "sp ace", "unic???de", "qu?stion")) { + assertThat(expand("/users/{user}", mapOf("user", val))) + .isEqualTo("/users/" + val); + } } @Test public void expandMultipleParams() { - assertEquals("/users/unic???de/foo", - expand("/users/{user}/{repo}", ImmutableMap.of("user", "unic???de", "repo", "foo"))); + assertThat(expand("/users/{user}/{repo}", mapOf("user", "unic???de", "repo", "foo"))) + .isEqualTo("/users/unic???de/foo"); } @Test public void expandParamKeyHyphen() { - assertEquals("/foo", expand("/{user-dir}", ImmutableMap.of("user-dir", "foo"))); + assertThat(expand("/{user-dir}", mapOf("user-dir", "foo"))) + .isEqualTo("/foo"); } @Test public void expandMissingParamProceeds() { - assertEquals("/{user-dir}", expand("/{user-dir}", ImmutableMap.of("user_dir", "foo"))); + assertThat(expand("/{user-dir}", mapOf("user_dir", "foo"))) + .isEqualTo("/{user-dir}"); } @Test public void resolveTemplateWithParameterizedPathSkipsEncodingSlash() { - RequestTemplate template = new RequestTemplate().method("GET") .append("{zoneId}"); - assertEquals("GET {zoneId} HTTP/1.1\n", template.toString()); + template.resolve(mapOf("zoneId", "/hostedzone/Z1PA6795UKMFR9")); - template.resolve(ImmutableMap.of("zoneId", "/hostedzone/Z1PA6795UKMFR9")); + assertThat(template) + .hasUrl("/hostedzone/Z1PA6795UKMFR9"); + } - assertEquals("GET /hostedzone/Z1PA6795UKMFR9 HTTP/1.1\n", template.toString()); + @Test public void canInsertAbsoluteHref() { + RequestTemplate template = new RequestTemplate().method("GET") + .append("/hostedzone/Z1PA6795UKMFR9"); template.insert(0, "https://route53.amazonaws.com/2012-12-12"); - assertEquals("GET https://route53.amazonaws.com/2012-12-12/hostedzone/Z1PA6795UKMFR9 HTTP/1.1\n", - template.request().toString()); + assertThat(template) + .hasUrl("https://route53.amazonaws.com/2012-12-12/hostedzone/Z1PA6795UKMFR9"); } @Test public void resolveTemplateWithBaseAndParameterizedQuery() { RequestTemplate template = new RequestTemplate().method("GET") .append("/?Action=DescribeRegions").query("RegionName.1", "{region}"); - assertEquals( - ImmutableListMultimap.of("Action", "DescribeRegions", "RegionName.1", "{region}").asMap(), - template.queries()); - assertEquals("GET /?Action=DescribeRegions&RegionName.1={region} HTTP/1.1\n", - template.toString()); - - template.resolve(ImmutableMap.of("region", "eu-west-1")); - assertEquals( - ImmutableListMultimap.of("Action", "DescribeRegions", "RegionName.1", "eu-west-1").asMap(), - template.queries()); - - assertEquals("GET /?Action=DescribeRegions&RegionName.1=eu-west-1 HTTP/1.1\n", - template.toString()); + template.resolve(mapOf("region", "eu-west-1")); - template.insert(0, "https://iam.amazonaws.com"); - - assertEquals( - "GET https://iam.amazonaws.com/?Action=DescribeRegions&RegionName.1=eu-west-1 HTTP/1.1\n", - template.request().toString()); + assertThat(template) + .hasQueries( + entry("Action", asList("DescribeRegions")), + entry("RegionName.1", asList("eu-west-1")) + ); } @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. builder() - .put("Query", "one") - .putAll("Queries", "us-east-1", "eu-west-1") - .build().asMap()); + template.resolve(mapOf("queries", Arrays.asList("us-east-1", "eu-west-1"))); - assertEquals("GET /?Query=one&Queries=us-east-1&Queries=eu-west-1 HTTP/1.1\n", template.toString()); + assertThat(template) + .hasQueries( + entry("Query", asList("one")), + entry("Queries", asList("us-east-1", "eu-west-1")) + ); } @Test public void resolveTemplateWithMixedRequestLineParams() throws Exception { @@ -106,44 +101,33 @@ ImmutableListMultimap. builder() .query("name", "{name}")// .query("type", "{type}"); - template = template.resolve(ImmutableMap.builder()// - .put("domainId", 1001)// - .put("name", "denominator.io")// - .put("type", "CNAME")// - .build() + template = template.resolve( + mapOf("domainId", 1001, "name", "denominator.io", "type", "CNAME") ); - assertEquals("GET /domains/1001/records?name=denominator.io&type=CNAME HTTP/1.1\n", - template.toString()); - - template.insert(0, "https://dns.api.rackspacecloud.com/v1.0/1234"); - - assertEquals(""// - + "GET https://dns.api.rackspacecloud.com/v1.0/1234"// - + "/domains/1001/records?name=denominator.io&type=CNAME HTTP/1.1\n", - template.request().toString()); + assertThat(template) + .hasUrl("/domains/1001/records") + .hasQueries( + entry("name", asList("denominator.io")), + entry("type", asList("CNAME")) + ); } @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.builder()// - .put("domainId", 1001)// - .put("name", "denominator.io")// - .put("type", "CNAME")// - .build() - ); - - assertEquals("GET /domains/1001/records?name=denominator.io&type=CNAME HTTP/1.1\n", - template.toString()); + .append("/domains/1001/records")// + .query("name", "denominator.io")// + .query("type", "CNAME"); template.insert(0, "https://host/v1.0/1234?provider=foo"); - assertEquals("GET https://host/v1.0/1234/domains/1001/records?provider=foo&name=denominator.io&type=CNAME HTTP/1.1\n", - template.request().toString()); + assertThat(template) + .hasUrl("https://host/v1.0/1234/domains/1001/records") + .hasQueries( + entry("provider", asList("foo")), + entry("name", asList("denominator.io")), + entry("type", asList("CNAME")) + ); } @Test public void resolveTemplateWithBodyTemplateSetsBodyAndContentLength() { @@ -151,28 +135,19 @@ ImmutableListMultimap. builder() .bodyTemplate("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", " + "\"password\": \"{password}\"%7D"); - template = template.resolve(ImmutableMap.builder()// - .put("customer_name", "netflix")// - .put("user_name", "denominator")// - .put("password", "password")// - .build() + template = template.resolve( + mapOf( + "customer_name", "netflix", + "user_name", "denominator", + "password", "password" + ) ); - assertEquals(""// - + "POST HTTP/1.1\n"// - + "Content-Length: 80\n"// - + "\n"// - + "{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}", - template.toString()); - - template.insert(0, "https://api2.dynect.net/REST"); - - assertEquals(""// - + "POST https://api2.dynect.net/REST HTTP/1.1\n" // - + "Content-Length: 80\n" // - + "\n" // - + "{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}", - template.request().toString()); + assertThat(template) + .hasBody("{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}") + .hasHeaders( + entry("Content-Length", asList(String.valueOf(template.body().length))) + ); } @Test public void skipUnresolvedQueries() throws Exception { @@ -181,14 +156,17 @@ ImmutableListMultimap. builder() .query("optional", "{optional}")// .query("name", "{nameVariable}"); - template = template.resolve(ImmutableMap.builder()// - .put("domainId", 1001)// - .put("nameVariable", "denominator.io")// - .build() + template = template.resolve(mapOf( + "domainId", 1001, + "nameVariable", "denominator.io" + ) ); - assertEquals("GET /domains/1001/records?name=denominator.io HTTP/1.1\n", - template.toString()); + assertThat(template) + .hasUrl("/domains/1001/records") + .hasQueries( + entry("name", asList("denominator.io")) + ); } @Test public void allQueriesUnresolvable() throws Exception { @@ -197,11 +175,29 @@ ImmutableListMultimap. builder() .query("optional", "{optional}")// .query("optional2", "{optional2}"); - template = template.resolve(ImmutableMap.builder()// - .put("domainId", 1001)// - .build() - ); + template = template.resolve(mapOf("domainId", 1001)); + + assertThat(template) + .hasUrl("/domains/1001/records") + .hasQueries(); + } + + /** Avoid depending on guava solely for map literals. */ + private static Map mapOf(String key, Object val) { + Map result = new LinkedHashMap(); + result.put(key, val); + return result; + } + + private static Map mapOf(String k1, Object v1, String k2, Object v2) { + Map result = mapOf(k1, v1); + result.put(k2, v2); + return result; + } - assertEquals("GET /domains/1001/records HTTP/1.1\n", template.toString()); + private static Map mapOf(String k1, Object v1, String k2, Object v2, String k3, Object v3) { + Map result = mapOf(k1, v1, k2, v2); + result.put(k3, v3); + return result; } } diff --git a/core/src/test/java/feign/TrustingSSLSocketFactory.java b/core/src/test/java/feign/TrustingSSLSocketFactory.java index 15d3eae6e2..98723a1cbb 100644 --- a/core/src/test/java/feign/TrustingSSLSocketFactory.java +++ b/core/src/test/java/feign/TrustingSSLSocketFactory.java @@ -15,13 +15,6 @@ */ 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; @@ -33,8 +26,8 @@ import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.Arrays; - -import javax.inject.Provider; +import java.util.LinkedHashMap; +import java.util.Map; import javax.net.ssl.KeyManager; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; @@ -50,20 +43,17 @@ */ final class TrustingSSLSocketFactory extends SSLSocketFactory implements X509TrustManager, X509KeyManager { - private static LoadingCache sslSocketFactories = - CacheBuilder.newBuilder().build(new CacheLoader() { - @Override - public SSLSocketFactory load(String serverAlias) throws Exception { - return new TrustingSSLSocketFactory(serverAlias); - } - }); + private static final Map sslSocketFactories = new LinkedHashMap(); public static SSLSocketFactory get() { return get(""); } - public static SSLSocketFactory get(String serverAlias) { - return sslSocketFactories.getUnchecked(serverAlias); + public synchronized static SSLSocketFactory get(String serverAlias) { + if (!sslSocketFactories.containsKey(serverAlias)) { + sslSocketFactories.put(serverAlias, new TrustingSSLSocketFactory(serverAlias)); + } + return sslSocketFactories.get(serverAlias); } private static final char[] KEYSTORE_PASSWORD = "password".toCharArray(); @@ -87,7 +77,7 @@ private TrustingSSLSocketFactory(String serverAlias) { this.certificateChain = null; } else { try { - KeyStore keyStore = loadKeyStore(Resources.newInputStreamSupplier(Resources.getResource("keystore.jks"))); + KeyStore keyStore = loadKeyStore(TrustingSSLSocketFactory.class.getResourceAsStream("/keystore.jks")); this.privateKey = (PrivateKey) keyStore.getKey(serverAlias, KEYSTORE_PASSWORD); Certificate[] rawChain = keyStore.getCertificateChain(serverAlias); this.certificateChain = Arrays.copyOf(rawChain, rawChain.length, X509Certificate[].class); @@ -175,17 +165,15 @@ public PrivateKey getPrivateKey(String alias) { return privateKey; } - private static KeyStore loadKeyStore(InputSupplier inputStreamSupplier) throws IOException { - Closer closer = Closer.create(); + private static KeyStore loadKeyStore(InputStream inputStream) throws IOException { 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); + } catch (Exception e) { + throw new RuntimeException(e); } finally { - closer.close(); + inputStream.close(); } } diff --git a/core/src/test/java/feign/assertj/FeignAssertions.java b/core/src/test/java/feign/assertj/FeignAssertions.java new file mode 100644 index 0000000000..821a80b851 --- /dev/null +++ b/core/src/test/java/feign/assertj/FeignAssertions.java @@ -0,0 +1,10 @@ +package feign.assertj; + +import feign.RequestTemplate; +import org.assertj.core.api.Assertions; + +public class FeignAssertions extends Assertions { + public static RequestTemplateAssert assertThat(RequestTemplate actual) { + return new RequestTemplateAssert(actual); + } +} diff --git a/core/src/test/java/feign/assertj/MockWebServerAssertions.java b/core/src/test/java/feign/assertj/MockWebServerAssertions.java new file mode 100644 index 0000000000..e6cbb1c146 --- /dev/null +++ b/core/src/test/java/feign/assertj/MockWebServerAssertions.java @@ -0,0 +1,10 @@ +package feign.assertj; + +import com.squareup.okhttp.mockwebserver.RecordedRequest; +import org.assertj.core.api.Assertions; + +public class MockWebServerAssertions extends Assertions { + public static RecordedRequestAssert assertThat(RecordedRequest actual) { + return new RecordedRequestAssert(actual); + } +} diff --git a/core/src/test/java/feign/assertj/RecordedRequestAssert.java b/core/src/test/java/feign/assertj/RecordedRequestAssert.java new file mode 100644 index 0000000000..54a2c3a65f --- /dev/null +++ b/core/src/test/java/feign/assertj/RecordedRequestAssert.java @@ -0,0 +1,87 @@ +package feign.assertj; + +import com.squareup.okhttp.mockwebserver.RecordedRequest; +import feign.Util; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.zip.GZIPInputStream; +import org.assertj.core.api.AbstractAssert; +import org.assertj.core.internal.ByteArrays; +import org.assertj.core.internal.Failures; +import org.assertj.core.internal.Iterables; +import org.assertj.core.internal.Objects; + +import static org.assertj.core.error.ShouldNotContain.shouldNotContain; + +public final class RecordedRequestAssert extends AbstractAssert { + + ByteArrays arrays = ByteArrays.instance(); + Objects objects = Objects.instance(); + Iterables iterables = Iterables.instance(); + Failures failures = Failures.instance(); + + public RecordedRequestAssert(RecordedRequest actual) { + super(actual, RecordedRequestAssert.class); + } + + public RecordedRequestAssert hasMethod(String expected) { + isNotNull(); + objects.assertEqual(info, actual.getMethod(), expected); + return this; + } + + public RecordedRequestAssert hasPath(String expected) { + isNotNull(); + objects.assertEqual(info, actual.getPath(), expected); + return this; + } + + public RecordedRequestAssert hasBody(String utf8Expected) { + isNotNull(); + objects.assertEqual(info, actual.getUtf8Body(), utf8Expected); + return this; + } + + public RecordedRequestAssert hasGzippedBody(byte[] expectedUncompressed) { + isNotNull(); + byte[] compressedBody = actual.getBody(); + byte[] uncompressedBody; + try { + uncompressedBody = Util.toByteArray(new GZIPInputStream(new ByteArrayInputStream(compressedBody))); + } catch (IOException e) { + throw new RuntimeException(e); + } + arrays.assertContains(info, uncompressedBody, expectedUncompressed); + return this; + } + + public RecordedRequestAssert hasBody(byte[] expected) { + isNotNull(); + arrays.assertContains(info, actual.getBody(), expected); + return this; + } + + public RecordedRequestAssert hasHeaders(String... headers) { + isNotNull(); + iterables.assertContainsSubsequence(info, actual.getHeaders(), headers); + return this; + } + + public RecordedRequestAssert hasNoHeaderNamed(final String... names) { + isNotNull(); + Set found = new LinkedHashSet(); + for (String header : actual.getHeaders()) { + for (String name : names) { + if (header.toLowerCase().startsWith(name.toLowerCase() + ":")) { + found.add(header); + } + } + } + if (found.isEmpty()) { + return this; + } + throw failures.failure(info, shouldNotContain(actual.getHeaders(), names, found)); + } +} diff --git a/core/src/test/java/feign/assertj/RequestTemplateAssert.java b/core/src/test/java/feign/assertj/RequestTemplateAssert.java new file mode 100644 index 0000000000..d2e9a26af6 --- /dev/null +++ b/core/src/test/java/feign/assertj/RequestTemplateAssert.java @@ -0,0 +1,67 @@ +package feign.assertj; + +import feign.RequestTemplate; +import org.assertj.core.api.AbstractAssert; +import org.assertj.core.data.MapEntry; +import org.assertj.core.internal.ByteArrays; +import org.assertj.core.internal.Maps; +import org.assertj.core.internal.Objects; + +import static feign.Util.UTF_8; + +public final class RequestTemplateAssert extends AbstractAssert { + + ByteArrays arrays = ByteArrays.instance(); + Objects objects = Objects.instance(); + Maps maps = Maps.instance(); + + public RequestTemplateAssert(RequestTemplate actual) { + super(actual, RequestTemplateAssert.class); + } + + public RequestTemplateAssert hasMethod(String expected) { + isNotNull(); + objects.assertEqual(info, actual.method(), expected); + return this; + } + + public RequestTemplateAssert hasUrl(String expected) { + isNotNull(); + objects.assertEqual(info, actual.url(), expected); + return this; + } + + public RequestTemplateAssert hasBody(String utf8Expected) { + return hasBody(utf8Expected.getBytes(UTF_8)); + } + + public RequestTemplateAssert hasBody(byte[] expected) { + isNotNull(); + if (actual.bodyTemplate() != null) { + failWithMessage("\nExpecting bodyTemplate to be null, but was:<%s>", actual.bodyTemplate()); + } + arrays.assertContains(info, actual.body(), expected); + return this; + } + + public RequestTemplateAssert hasBodyTemplate(String expected) { + isNotNull(); + if (actual.body() != null) { + failWithMessage("\nExpecting body to be null, but was:<%s>", actual.bodyTemplate()); + } + objects.assertEqual(info, actual.bodyTemplate(), expected); + return this; + } + + public RequestTemplateAssert hasQueries(MapEntry... entries) { + isNotNull(); + maps.assertContainsExactly(info, actual.queries(), entries); + return this; + } + + public RequestTemplateAssert hasHeaders(MapEntry... entries) { + isNotNull(); + maps.assertContainsExactly(info, actual.headers(), entries); + return this; + } +} diff --git a/core/src/test/java/feign/auth/BasicAuthRequestInterceptorTest.java b/core/src/test/java/feign/auth/BasicAuthRequestInterceptorTest.java index 56745b0ccf..ab332951fc 100644 --- a/core/src/test/java/feign/auth/BasicAuthRequestInterceptorTest.java +++ b/core/src/test/java/feign/auth/BasicAuthRequestInterceptorTest.java @@ -19,33 +19,33 @@ import java.util.Collections; import org.junit.Test; +import static feign.assertj.FeignAssertions.assertThat; +import static java.util.Arrays.asList; +import static org.assertj.core.data.MapEntry.entry; import static org.junit.Assert.assertEquals; -/** - * Tests for {@link BasicAuthRequestInterceptor}. - */ public class BasicAuthRequestInterceptorTest { - /** - * Tests that request headers are added as expected. - */ - @Test public void testAuthentication() { + + @Test public void addsAuthorizationHeader() { RequestTemplate template = new RequestTemplate(); BasicAuthRequestInterceptor interceptor = new BasicAuthRequestInterceptor("Aladdin", "open sesame"); interceptor.apply(template); - assertEquals(Collections.singletonList("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="), - template.headers().get("Authorization")); + + assertThat(template) + .hasHeaders( + entry("Authorization", asList("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==")) + ); } - /** - * Tests that requests headers are added as expected when user and pass are too long - */ - @Test public void testAuthenticationWhenUserPassAreTooLong() { + @Test public void addsAuthorizationHeader_longUserAndPassword() { RequestTemplate template = new RequestTemplate(); BasicAuthRequestInterceptor interceptor = new BasicAuthRequestInterceptor("IOIOIOIOIOIOIOIOIOIOIOIOIOIOIOIOIOIOIO", "101010101010101010101010101010101010101010"); interceptor.apply(template); - assertEquals(Collections.singletonList( - "Basic SU9JT0lPSU9JT0lPSU9JT0lPSU9JT0lPSU9JT0lPSU9JT0lPSU86MTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEw"), - template.headers().get("Authorization")); + + assertThat(template) + .hasHeaders( + entry("Authorization", asList("Basic SU9JT0lPSU9JT0lPSU9JT0lPSU9JT0lPSU9JT0lPSU9JT0lPSU86MTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEw")) + ); } } diff --git a/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java b/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java index f3814389a7..d78b022e12 100644 --- a/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java +++ b/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java @@ -15,11 +15,12 @@ */ package feign.codec; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableMultimap; import feign.FeignException; import feign.Response; +import java.util.Arrays; import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -32,12 +33,13 @@ public class DefaultErrorDecoderTest { ErrorDecoder errorDecoder = new ErrorDecoder.Default(); + Map> headers = new LinkedHashMap>(); + @Test public void throwsFeignException() throws Throwable { thrown.expect(FeignException.class); thrown.expectMessage("status 500 reading Service#foo()"); - - Response response = Response.create(500, "Internal server error", ImmutableMap.>of(), - null); + + Response response = Response.create(500, "Internal server error", headers, null); throw errorDecoder.decode("Service#foo()", response); } @@ -46,8 +48,7 @@ public class DefaultErrorDecoderTest { thrown.expect(FeignException.class); thrown.expectMessage("status 500 reading Service#foo(); content:\nhello world"); - Response response = Response.create(500, "Internal server error", ImmutableMap.>of(), - "hello world", UTF_8); + Response response = Response.create(500, "Internal server error", headers, "hello world", UTF_8); throw errorDecoder.decode("Service#foo()", response); } @@ -56,8 +57,8 @@ public class DefaultErrorDecoderTest { thrown.expect(FeignException.class); thrown.expectMessage("status 503 reading Service#foo()"); - Response response = Response.create(503, "Service Unavailable", - ImmutableMultimap.of(RETRY_AFTER, "Sat, 1 Jan 2000 00:00:00 GMT").asMap(), null); + headers.put(RETRY_AFTER, Arrays.asList("Sat, 1 Jan 2000 00:00:00 GMT")); + Response response = Response.create(503, "Service Unavailable", headers, null); throw errorDecoder.decode("Service#foo()", response); } diff --git a/gson/build.gradle b/gson/build.gradle index c0a064acd2..836ea53cdb 100644 --- a/gson/build.gradle +++ b/gson/build.gradle @@ -6,4 +6,6 @@ dependencies { compile project(':feign-core') compile 'com.google.code.gson:gson:2.2.4' testCompile 'junit:junit:4.12' + testCompile 'org.assertj:assertj-core:1.7.1' + testCompile project(':feign-core').sourceSets.test.output // for assertions } diff --git a/gson/src/test/java/feign/gson/GsonModuleTest.java b/gson/src/test/java/feign/gson/GsonModuleTest.java index 341a4c3519..fdf95cf0cb 100644 --- a/gson/src/test/java/feign/gson/GsonModuleTest.java +++ b/gson/src/test/java/feign/gson/GsonModuleTest.java @@ -38,6 +38,7 @@ import org.junit.Test; import static feign.Util.UTF_8; +import static feign.assertj.FeignAssertions.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; @@ -62,11 +63,6 @@ static class EncoderBindings { } @Test public void encodesMapObjectNumericalValuesAsInteger() throws Exception { - String expectedBody = "" - + "{\n" - + " \"foo\": 1\n" - + "}"; - EncoderBindings bindings = new EncoderBindings(); ObjectGraph.create(bindings).inject(bindings); @@ -75,18 +71,14 @@ static class EncoderBindings { RequestTemplate template = new RequestTemplate(); bindings.encoder.encode(map, template); - assertEquals(expectedBody, new String(template.body(), UTF_8)); + + assertThat(template).hasBody("" // + + "{\n" // + + " \"foo\": 1\n" // + + "}"); } @Test public void encodesFormParams() throws Exception { - String expectedBody = ""// - + "{\n" // - + " \"foo\": 1,\n" // - + " \"bar\": [\n" // - + " 2,\n" // - + " 3\n" // - + " ]\n" // - + "}"; EncoderBindings bindings = new EncoderBindings(); ObjectGraph.create(bindings).inject(bindings); @@ -97,7 +89,15 @@ static class EncoderBindings { RequestTemplate template = new RequestTemplate(); bindings.encoder.encode(form, template); - assertEquals(expectedBody, new String(template.body(), UTF_8)); + + assertThat(template).hasBody("" // + + "{\n" // + + " \"foo\": 1,\n" // + + " \"bar\": [\n" // + + " 2,\n" // + + " 3\n" // + + " ]\n" // + + "}"); } static class Zone extends LinkedHashMap { diff --git a/jackson/build.gradle b/jackson/build.gradle index ca2414cac9..d8b7ea9f38 100644 --- a/jackson/build.gradle +++ b/jackson/build.gradle @@ -6,5 +6,6 @@ dependencies { compile project(':feign-core') compile 'com.fasterxml.jackson.core:jackson-databind:2.2.2' testCompile 'junit:junit:4.12' - testCompile 'com.google.guava:guava:14.0.1' + testCompile 'org.assertj:assertj-core:1.7.1' + testCompile project(':feign-core').sourceSets.test.output // for assertions } diff --git a/jackson/src/test/java/feign/jackson/JacksonModuleTest.java b/jackson/src/test/java/feign/jackson/JacksonModuleTest.java index 22bcb2251d..2aa8f9f0a7 100644 --- a/jackson/src/test/java/feign/jackson/JacksonModuleTest.java +++ b/jackson/src/test/java/feign/jackson/JacksonModuleTest.java @@ -2,10 +2,10 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import com.fasterxml.jackson.databind.module.SimpleModule; -import com.google.common.reflect.TypeToken; import dagger.Module; import dagger.ObjectGraph; import dagger.Provides; @@ -25,6 +25,7 @@ import org.junit.Test; import static feign.Util.UTF_8; +import static feign.assertj.FeignAssertions.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; @@ -60,10 +61,11 @@ static class EncoderBindings { RequestTemplate template = new RequestTemplate(); bindings.encoder.encode(map, template); - assertEquals(""// + + assertThat(template).hasBody(""// + "{\n" // + " \"foo\" : 1\n" // - + "}", new String(template.body(), UTF_8)); + + "}"); } @Test public void encodesFormParams() throws Exception { @@ -76,11 +78,12 @@ static class EncoderBindings { RequestTemplate template = new RequestTemplate(); bindings.encoder.encode(form, template); - assertEquals(""// + + assertThat(template).hasBody(""// + "{\n" // + " \"foo\" : 1,\n" // + " \"bar\" : [ 2, 3 ]\n" // - + "}", new String(template.body(), UTF_8)); + + "}"); } static class Zone extends LinkedHashMap { @@ -117,7 +120,7 @@ static class DecoderBindings { Response response = Response.create(200, "OK", Collections.>emptyMap(), zonesJson, UTF_8); - assertEquals(zones, bindings.decoder.decode(response, new TypeToken>() { + assertEquals(zones, bindings.decoder.decode(response, new TypeReference>() { }.getType())); } @@ -186,7 +189,7 @@ com.fasterxml.jackson.databind.Module upperZone() { Response response = Response.create(200, "OK", Collections.>emptyMap(), zonesJson, UTF_8); - assertEquals(zones, bindings.decoder.decode(response, new TypeToken>() { + assertEquals(zones, bindings.decoder.decode(response, new TypeReference>() { }.getType())); } } diff --git a/jaxb/build.gradle b/jaxb/build.gradle index 4dda750faa..fda54f4a16 100644 --- a/jaxb/build.gradle +++ b/jaxb/build.gradle @@ -3,5 +3,6 @@ apply plugin: 'java' dependencies { compile project(':feign-core') testCompile 'junit:junit:4.12' - testCompile 'com.google.guava:guava:14.0.1' + testCompile 'org.assertj:assertj-core:1.7.1' + testCompile project(':feign-core').sourceSets.test.output // for assertions } diff --git a/jaxb/src/test/java/feign/jaxb/JAXBModuleTest.java b/jaxb/src/test/java/feign/jaxb/JAXBModuleTest.java index ec40f104f9..bc0ed745c0 100644 --- a/jaxb/src/test/java/feign/jaxb/JAXBModuleTest.java +++ b/jaxb/src/test/java/feign/jaxb/JAXBModuleTest.java @@ -15,24 +15,23 @@ */ package feign.jaxb; -import com.google.common.reflect.TypeToken; import dagger.Module; import dagger.ObjectGraph; import feign.RequestTemplate; import feign.Response; import feign.codec.Decoder; import feign.codec.Encoder; - +import java.util.Collection; +import java.util.Collections; import javax.inject.Inject; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; -import java.util.Collection; -import java.util.Collections; import org.junit.Test; import static feign.Util.UTF_8; +import static feign.assertj.FeignAssertions.assertThat; import static org.junit.Assert.assertEquals; public class JAXBModuleTest { @@ -71,24 +70,13 @@ static class MockObject { @XmlElement private String value; - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - MockObject that = (MockObject) o; - - if (value != null ? !value.equals(that.value) : that.value != null) return false; - - return true; + public boolean equals(Object obj) { + if (obj instanceof MockObject) { + MockObject other = (MockObject) obj; + return value.equals(other.value); + } + return false; } @Override @@ -103,14 +91,13 @@ public void encodesXml() throws Exception { ObjectGraph.create(bindings).inject(bindings); MockObject mock = new MockObject(); - mock.setValue("Test"); + mock.value = "Test"; RequestTemplate template = new RequestTemplate(); bindings.encoder.encode(mock, template); - assertEquals( - "Test", - new String(template.body(), UTF_8)); + assertThat(template).hasBody( + "Test"); } @Test @@ -123,14 +110,13 @@ public void encodesXmlWithCustomJAXBEncoding() throws Exception { Encoder encoder = jaxbModule.encoder(new JAXBEncoder(jaxbContextFactory)); MockObject mock = new MockObject(); - mock.setValue("Test"); + mock.value = "Test"; RequestTemplate template = new RequestTemplate(); encoder.encode(mock, template); - assertEquals("Test", - new String(template.body(), UTF_8)); + assertThat(template).hasBody("Test"); } @Test @@ -143,15 +129,15 @@ public void encodesXmlWithCustomJAXBSchemaLocation() throws Exception { Encoder encoder = jaxbModule.encoder(new JAXBEncoder(jaxbContextFactory)); MockObject mock = new MockObject(); - mock.setValue("Test"); + mock.value = "Test"; RequestTemplate template = new RequestTemplate(); encoder.encode(mock, template); - assertEquals("" + - "Test", new String(template.body(), UTF_8)); + assertThat(template).hasBody("" + + "Test"); } @Test @@ -164,15 +150,15 @@ public void encodesXmlWithCustomJAXBNoNamespaceSchemaLocation() throws Exception Encoder encoder = jaxbModule.encoder(new JAXBEncoder(jaxbContextFactory)); MockObject mock = new MockObject(); - mock.setValue("Test"); + mock.value = "Test"; RequestTemplate template = new RequestTemplate(); encoder.encode(mock, template); - assertEquals("" + - "Test", new String(template.body(), UTF_8)); + assertThat(template).hasBody("" + + "Test"); } @Test @@ -185,20 +171,18 @@ public void encodesXmlWithCustomJAXBFormattedOutput() { Encoder encoder = jaxbModule.encoder(new JAXBEncoder(jaxbContextFactory)); MockObject mock = new MockObject(); - mock.setValue("Test"); + mock.value = "Test"; RequestTemplate template = new RequestTemplate(); encoder.encode(mock, template); String NEWLINE = System.getProperty("line.separator"); - StringBuilder expectedXml = new StringBuilder(); - expectedXml.append("").append(NEWLINE) + assertThat(template).hasBody(new StringBuilder() + .append("").append(NEWLINE) .append("").append(NEWLINE) .append(" Test").append(NEWLINE) - .append("").append(NEWLINE); - - assertEquals(expectedXml.toString(), new String(template.body(), UTF_8)); + .append("").append(NEWLINE).toString()); } @Test @@ -207,7 +191,7 @@ public void decodesXml() throws Exception { ObjectGraph.create(bindings).inject(bindings); MockObject mock = new MockObject(); - mock.setValue("Test"); + mock.value = "Test"; String mockXml = "" + "Test"; @@ -215,6 +199,6 @@ public void decodesXml() throws Exception { Response response = Response.create(200, "OK", Collections.>emptyMap(), mockXml, UTF_8); - assertEquals(mock, bindings.decoder.decode(response, new TypeToken() {}.getType())); + assertEquals(mock, bindings.decoder.decode(response, MockObject.class)); } } diff --git a/jaxb/src/test/java/feign/jaxb/examples/AWSSignatureVersion4.java b/jaxb/src/test/java/feign/jaxb/examples/AWSSignatureVersion4.java index 0d9e3b84b5..aaacbe71bc 100644 --- a/jaxb/src/test/java/feign/jaxb/examples/AWSSignatureVersion4.java +++ b/jaxb/src/test/java/feign/jaxb/examples/AWSSignatureVersion4.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 Netflix, Inc. + * 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. @@ -15,30 +15,20 @@ */ package feign.jaxb.examples; -import com.google.common.base.Function; -import com.google.common.base.Joiner; -import com.google.common.collect.Multimap; -import com.google.common.collect.TreeMultimap; import feign.Request; import feign.RequestTemplate; - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; import java.net.URI; +import java.security.MessageDigest; import java.text.SimpleDateFormat; -import java.util.Collection; import java.util.Date; -import java.util.Map.Entry; import java.util.TimeZone; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; -import static com.google.common.base.Throwables.propagate; -import static com.google.common.collect.Iterables.transform; -import static com.google.common.hash.Hashing.sha256; -import static com.google.common.io.BaseEncoding.base16; import static feign.Util.UTF_8; // http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html -public class AWSSignatureVersion4 implements Function { +public class AWSSignatureVersion4 { String region = "us-east-1"; String service = "iam"; @@ -50,31 +40,30 @@ public AWSSignatureVersion4(String accessKey, String secretKey) { this.secretKey = secretKey; } - @Override public Request apply(RequestTemplate input) { - input.header("Host", URI.create(input.url()).getHost()); - TreeMultimap sortedLowercaseHeaders = TreeMultimap.create(); - for (String key : input.headers().keySet()) { - sortedLowercaseHeaders.putAll(trimToLowercase.apply(key), - transform(input.headers().get(key), trimToLowercase)); - } + public Request apply(RequestTemplate input) { + if (!input.headers().isEmpty()) throw new UnsupportedOperationException("headers not supported"); + if (input.body() != null) throw new UnsupportedOperationException("body not supported"); + + String host = URI.create(input.url()).getHost(); String timestamp; synchronized (iso8601) { timestamp = iso8601.format(new Date()); } - String credentialScope = Joiner.on('/').join(timestamp.substring(0, 8), region, service, "aws4_request"); + String credentialScope = String.format("%s/%s/%s/%s", timestamp.substring(0, 8), region, service, "aws4_request"); input.query("X-Amz-Algorithm", "AWS4-HMAC-SHA256"); input.query("X-Amz-Credential", accessKey + "/" + credentialScope); input.query("X-Amz-Date", timestamp); - input.query("X-Amz-SignedHeaders", Joiner.on(';').join(sortedLowercaseHeaders.keySet())); + input.query("X-Amz-SignedHeaders", "host"); + input.header("Host", host); - String canonicalString = canonicalString(input, sortedLowercaseHeaders); + String canonicalString = canonicalString(input, host); String toSign = toSign(timestamp, credentialScope, canonicalString); byte[] signatureKey = signatureKey(secretKey, timestamp); - String signature = base16().lowerCase().encode(hmacSHA256(toSign, signatureKey)); + String signature = hex(hmacSHA256(toSign, signatureKey)); input.query("X-Amz-Signature", signature); @@ -97,13 +86,13 @@ static byte[] hmacSHA256(String data, byte[] key) { mac.init(new SecretKeySpec(key, algorithm)); return mac.doFinal(data.getBytes(UTF_8)); } catch (Exception e) { - throw propagate(e); + throw new RuntimeException(e); } } private static final String EMPTY_STRING_HASH = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; - private String canonicalString(RequestTemplate input, Multimap sortedLowercaseHeaders) { + private static String canonicalString(RequestTemplate input, String host) { StringBuilder canonicalRequest = new StringBuilder(); // HTTPRequestMethod + '\n' + canonicalRequest.append(input.method()).append('\n'); @@ -116,33 +105,25 @@ private String canonicalString(RequestTemplate input, Multimap s canonicalRequest.append('\n'); // CanonicalHeaders + '\n' + - for (Entry> entry : sortedLowercaseHeaders.asMap().entrySet()) { - canonicalRequest.append(entry.getKey()).append(':').append(Joiner.on(',').join(entry.getValue())) - .append('\n'); - } + canonicalRequest.append("host:").append(host).append('\n'); + canonicalRequest.append('\n'); // SignedHeaders + '\n' + - canonicalRequest.append(Joiner.on(',').join(sortedLowercaseHeaders.keySet())).append('\n'); + canonicalRequest.append("host").append('\n'); // HexEncode(Hash(Payload)) String bodyText = - input.charset() != null && input.body() != null ? new String(input.body(), input.charset()) : null; + input.charset() != null && input.body() != null ? new String(input.body(), input.charset()) : null; if (bodyText != null) { - canonicalRequest.append(base16().lowerCase().encode(sha256().hashString(bodyText, UTF_8).asBytes())); + canonicalRequest.append(hex(sha256(bodyText))); } else { canonicalRequest.append(EMPTY_STRING_HASH); } return canonicalRequest.toString(); } - private static final Function trimToLowercase = new Function() { - public String apply(String in) { - return in == null ? null : in.toLowerCase().trim(); - } - }; - - private String toSign(String timestamp, String credentialScope, String canonicalRequest) { + private static String toSign(String timestamp, String credentialScope, String canonicalRequest) { StringBuilder toSign = new StringBuilder(); // Algorithm + '\n' + toSign.append("AWS4-HMAC-SHA256").append('\n'); @@ -151,10 +132,28 @@ private String toSign(String timestamp, String credentialScope, String canonical // CredentialScope + '\n' + toSign.append(credentialScope).append('\n'); // HexEncode(Hash(CanonicalRequest)) - toSign.append(base16().lowerCase().encode(sha256().hashString(canonicalRequest, UTF_8).asBytes())); + toSign.append(hex(sha256(canonicalRequest))); return toSign.toString(); } + + private static String hex(byte[] data) { + StringBuilder result = new StringBuilder(data.length * 2); + for (byte b : data) { + result.append(String.format("%02x", b & 0xff)); + } + return result.toString(); + } + + static byte[] sha256(String data) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + return digest.digest(data.getBytes(UTF_8)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + private static final SimpleDateFormat iso8601 = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"); static { diff --git a/jaxb/src/test/java/feign/jaxb/examples/IAMExample.java b/jaxb/src/test/java/feign/jaxb/examples/IAMExample.java index dd661017c2..cdf64245cf 100644 --- a/jaxb/src/test/java/feign/jaxb/examples/IAMExample.java +++ b/jaxb/src/test/java/feign/jaxb/examples/IAMExample.java @@ -22,7 +22,6 @@ import feign.Target; import feign.jaxb.JAXBContextFactory; import feign.jaxb.JAXBDecoder; - import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; @@ -41,8 +40,7 @@ public static void main(String... args) { .target(new IAMTarget(args[0], args[1])); GetUserResponse response = iam.userResponse(); - System.out.println("UserId: " + response.getUserResult().getUser().getUserId()); - System.out.println("UserName: " + response.getUserResult().getUser().getUsername()); + System.out.println("UserId: " + response.result.user.id); } static class IAMTarget extends AWSSignatureVersion4 implements Target { @@ -73,43 +71,7 @@ private IAMTarget(String accessKey, String secretKey) { @XmlAccessorType(XmlAccessType.FIELD) static class GetUserResponse { @XmlElement(name = "GetUserResult") - private GetUserResult userResult; - - @XmlElement(name = "ResponseMetadata") - private ResponseMetadata responseMetadata; - - public GetUserResult getUserResult() { - return userResult; - } - - public void setUserResult(GetUserResult userResult) { - this.userResult = userResult; - } - - public ResponseMetadata getResponseMetadata() { - return responseMetadata; - } - - public void setResponseMetadata(ResponseMetadata responseMetadata) { - this.responseMetadata = responseMetadata; - } - } - - @XmlAccessorType(XmlAccessType.FIELD) - @XmlType(name = "ResponseMetadata") - static class ResponseMetadata { - @XmlElement(name = "RequestId") - private String requestId; - - public ResponseMetadata() {} - - public String getRequestId() { - return requestId; - } - - public void setRequestId(String requestId) { - this.requestId = requestId; - } + private GetUserResult result; } @XmlAccessorType(XmlAccessType.FIELD) @@ -117,76 +79,12 @@ public void setRequestId(String requestId) { static class GetUserResult { @XmlElement(name = "User") private User user; - - public GetUserResult() {} - - public User getUser() { - return user; - } - - public void setUser(User user) { - this.user = user; - } } @XmlAccessorType(XmlAccessType.FIELD) @XmlType(name = "User") static class User { @XmlElement(name = "UserId") - private String userId; - - @XmlElement(name = "Path") - private String path; - - @XmlElement(name = "UserName") - private String username; - - @XmlElement(name = "Arn") - private String arn; - - @XmlElement(name = "CreateDate") - private String createDate; - - public User() {} - - public String getUserId() { - return userId; - } - - public void setUserId(String userId) { - this.userId = userId; - } - - public String getPath() { - return path; - } - - public void setPath(String path) { - this.path = path; - } - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getArn() { - return arn; - } - - public void setArn(String arn) { - this.arn = arn; - } - - public String getCreateDate() { - return createDate; - } - - public void setCreateDate(String createDate) { - this.createDate = createDate; - } + private String id; } } diff --git a/jaxrs/build.gradle b/jaxrs/build.gradle index fdb8f2dadb..fc5995bbf6 100644 --- a/jaxrs/build.gradle +++ b/jaxrs/build.gradle @@ -5,7 +5,8 @@ sourceCompatibility = 1.6 dependencies { compile project(':feign-core') compile 'javax.ws.rs:jsr311-api:1.1.1' - testCompile project(':feign-gson') - testCompile 'com.google.guava:guava:14.0.1' testCompile 'junit:junit:4.12' + testCompile 'org.assertj:assertj-core:1.7.1' + testCompile project(':feign-core').sourceSets.test.output // for assertions + testCompile project(':feign-gson') // for github example } diff --git a/jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java b/jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java index c9bd9878f2..a88fcb5536 100644 --- a/jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java +++ b/jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java @@ -15,7 +15,6 @@ */ package feign.jaxrs; -import com.google.gson.reflect.TypeToken; import feign.MethodMetadata; import feign.Response; import java.lang.annotation.ElementType; @@ -23,7 +22,6 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.net.URI; -import java.util.Arrays; import java.util.List; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; @@ -41,17 +39,9 @@ import org.junit.Test; import org.junit.rules.ExpectedException; -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.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; +import static feign.assertj.FeignAssertions.assertThat; +import static java.util.Arrays.asList; +import static org.assertj.core.data.MapEntry.entry; /** * Tests interfaces defined per {@link feign.jaxrs.JAXRSModule.JAXRSContract} are interpreted into expected {@link feign @@ -74,32 +64,33 @@ interface Methods { } @Test public void httpMethods() throws Exception { - assertEquals(POST, contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("post")).template().method()); - assertEquals(PUT, contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("put")).template().method()); - assertEquals(GET, contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("get")).template().method()); - assertEquals(DELETE, contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("delete")).template().method()); + assertThat(contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("post")).template()) + .hasMethod("POST"); + + assertThat(contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("put")).template()) + .hasMethod("PUT"); + + assertThat(contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("get")).template()) + .hasMethod("GET"); + + assertThat(contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("delete")).template()) + .hasMethod("DELETE"); } - interface CustomMethodAndURIParam { + interface CustomMethod { @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @HttpMethod("PATCH") public @interface PATCH { } - @PATCH Response patch(URI nextLink); + @PATCH Response patch(); } - @Test public void requestLineOnlyRequiresMethod() throws Exception { - MethodMetadata md = contract.parseAndValidatateMetadata(CustomMethodAndURIParam.class.getDeclaredMethod("patch", - URI.class)); - assertEquals("PATCH", md.template().method()); - assertEquals("", md.template().url()); - assertTrue(md.template().queries().isEmpty()); - assertTrue(md.template().headers().isEmpty()); - assertNull(md.template().body()); - assertNull(md.template().bodyTemplate()); - assertEquals(Integer.valueOf(0), md.urlIndex()); + @Test public void customMethodWithoutPath() throws Exception { + assertThat(contract.parseAndValidatateMetadata(CustomMethod.class.getDeclaredMethod("patch")).template()) + .hasMethod("PATCH") + .hasUrl(""); } interface WithQueryParamsInPath { @@ -115,51 +106,48 @@ interface WithQueryParamsInPath { } @Test public void queryParamsInPathExtract() throws Exception { - { - MethodMetadata md = contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("none")); - assertEquals("/", md.template().url()); - assertTrue(md.template().queries().isEmpty()); - assertEquals("GET / HTTP/1.1\n", md.template().toString()); - } - { - MethodMetadata md = contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("one")); - assertEquals("/", md.template().url()); - assertEquals(Arrays.asList("GetUser"), md.template().queries().get("Action")); - assertEquals("GET /?Action=GetUser HTTP/1.1\n", md.template().toString()); - } - { - MethodMetadata md = contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("two")); - assertEquals("/", md.template().url()); - assertEquals(Arrays.asList("GetUser"), md.template().queries().get("Action")); - assertEquals(Arrays.asList("2010-05-08"), md.template().queries().get("Version")); - assertEquals("GET /?Action=GetUser&Version=2010-05-08 HTTP/1.1\n", md.template().toString()); - } - { - MethodMetadata md = contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("three")); - assertEquals("/", md.template().url()); - assertEquals(Arrays.asList("GetUser"), md.template().queries().get("Action")); - assertEquals(Arrays.asList("2010-05-08"), md.template().queries().get("Version")); - assertEquals(Arrays.asList("1"), md.template().queries().get("limit")); - assertEquals("GET /?Action=GetUser&Version=2010-05-08&limit=1 HTTP/1.1\n", md.template().toString()); - } - { - MethodMetadata md = contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("empty")); - assertEquals("/", md.template().url()); - assertTrue(md.template().queries().containsKey("flag")); - assertEquals(Arrays.asList("GetUser"), md.template().queries().get("Action")); - assertEquals(Arrays.asList("2010-05-08"), md.template().queries().get("Version")); - assertEquals("GET /?flag&Action=GetUser&Version=2010-05-08 HTTP/1.1\n", md.template().toString()); - } + assertThat(contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("none")).template()) + .hasUrl("/") + .hasQueries(); + + assertThat(contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("one")).template()) + .hasUrl("/") + .hasQueries( + entry("Action", asList("GetUser")) + ); + + assertThat(contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("two")).template()) + .hasUrl("/") + .hasQueries( + entry("Action", asList("GetUser")), + entry("Version", asList("2010-05-08")) + ); + + assertThat(contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("three")).template()) + .hasUrl("/") + .hasQueries( + entry("Action", asList("GetUser")), + entry("Version", asList("2010-05-08")), + entry("limit", asList("1")) + ); + + assertThat(contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("empty")).template()) + .hasUrl("/") + .hasQueries( + entry("flag", asList(new String[] { null })), + entry("Action", asList("GetUser")), + entry("Version", asList("2010-05-08")) + ); } interface ProducesAndConsumes { - @GET @Produces(APPLICATION_XML) Response produces(); + @GET @Produces("application/xml") Response produces(); @GET @Produces({}) Response producesNada(); @GET @Produces({""}) Response producesEmpty(); - @POST @Consumes(APPLICATION_JSON) Response consumes(); + @POST @Consumes("application/xml") Response consumes(); @POST @Consumes({}) Response consumesNada(); @@ -168,7 +156,9 @@ interface ProducesAndConsumes { @Test public void producesAddsAcceptHeader() throws Exception { MethodMetadata md = contract.parseAndValidatateMetadata(ProducesAndConsumes.class.getDeclaredMethod("produces")); - assertEquals(Arrays.asList(APPLICATION_XML), md.template().headers().get(ACCEPT)); + + assertThat(md.template()) + .hasHeaders(entry("Accept", asList("application/xml"))); } @Test public void producesNada() throws Exception { @@ -187,7 +177,9 @@ interface ProducesAndConsumes { @Test public void consumesAddsContentTypeHeader() throws Exception { MethodMetadata md = contract.parseAndValidatateMetadata(ProducesAndConsumes.class.getDeclaredMethod("consumes")); - assertEquals(Arrays.asList(APPLICATION_JSON), md.template().headers().get(CONTENT_TYPE)); + + assertThat(md.template()) + .hasHeaders(entry("Content-Type", asList("application/xml"))); } @Test public void consumesNada() throws Exception { @@ -210,15 +202,16 @@ interface BodyParams { @POST Response tooMany(List body, List body2); } + private static final List STRING_LIST = null; + @Test public void bodyParamIsGeneric() throws Exception { MethodMetadata md = contract.parseAndValidatateMetadata(BodyParams.class.getDeclaredMethod("post", List.class)); - assertNull(md.template().body()); - assertNull(md.template().bodyTemplate()); - assertNull(md.urlIndex()); - assertEquals(Integer.valueOf(0), md.bodyIndex()); - assertEquals(new TypeToken>() { - }.getType(), md.bodyType()); + + assertThat(md.bodyIndex()) + .isEqualTo(0); + assertThat(md.bodyType()) + .isEqualTo(getClass().getDeclaredField("STRING_LIST").getGenericType()); } @Test public void tooManyBodies() throws Exception { @@ -249,43 +242,47 @@ interface BodyParams { @GET @Path("/{param}") Response emptyPathParam(@PathParam("") String empty); } - @Test public void pathOnType() throws Exception { - MethodMetadata md = contract.parseAndValidatateMetadata(PathOnType.class.getDeclaredMethod("base")); - assertEquals("/base", md.template().url()); - md = contract.parseAndValidatateMetadata(PathOnType.class.getDeclaredMethod("get")); - assertEquals("/base/specific", md.template().url()); + private MethodMetadata parsePathOnTypeMethod(String name) throws NoSuchMethodException { + return contract.parseAndValidatateMetadata(PathOnType.class.getDeclaredMethod(name)); + } + + @Test public void parsePathMethod() throws Exception { + assertThat(parsePathOnTypeMethod("base").template()) + .hasUrl("/base"); + + assertThat(parsePathOnTypeMethod("get").template()) + .hasUrl("/base/specific"); } @Test public void emptyPathOnMethod() throws Exception { thrown.expect(IllegalStateException.class); thrown.expectMessage("Path.value() was empty on method emptyPath"); - contract.parseAndValidatateMetadata(PathOnType.class.getDeclaredMethod("emptyPath")); + parsePathOnTypeMethod("emptyPath"); } @Test public void emptyPathParam() throws Exception { thrown.expect(IllegalStateException.class); thrown.expectMessage("PathParam.value() was empty on parameter 0"); - contract.parseAndValidatateMetadata(PathOnType.class.getDeclaredMethod("emptyPathParam", String.class)); + 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); } - @Test public void methodCanHaveUriParam() throws Exception { - MethodMetadata md = contract.parseAndValidatateMetadata(WithURIParam.class.getDeclaredMethod("uriParam", String.class, - URI.class, String.class)); - assertEquals(Integer.valueOf(1), md.urlIndex()); - } + @Test public void withPathAndURIParams() throws Exception { + MethodMetadata md = contract.parseAndValidatateMetadata( + WithURIParam.class.getDeclaredMethod("uriParam", String.class, URI.class, String.class)); + + assertThat(md.indexToName()).containsExactly( + entry(0, asList("1")), + // Skips 1 as it is a url index! + entry(2, asList("2"))); - @Test public void pathParamsParseIntoIndexToName() throws Exception { - MethodMetadata md = contract.parseAndValidatateMetadata(WithURIParam.class.getDeclaredMethod("uriParam", String.class, - URI.class, String.class)); - assertEquals("/{1}/{2}", md.template().url()); - assertEquals(Arrays.asList("1"), md.indexToName().get(0)); - assertEquals(Arrays.asList("2"), md.indexToName().get(2)); + assertThat(md.urlIndex()).isEqualTo(1); } interface WithPathAndQueryParams { @@ -293,29 +290,25 @@ interface WithPathAndQueryParams { Response recordsByNameAndType(@PathParam("domainId") int id, @QueryParam("name") String nameFilter, @QueryParam("type") String typeFilter); - @GET Response emptyQueryParam(@QueryParam("") String empty); + @GET Response empty(@QueryParam("") String empty); } - @Test public void mixedRequestLineParams() throws Exception { + @Test public void pathAndQueryParams() throws Exception { MethodMetadata md = contract.parseAndValidatateMetadata(WithPathAndQueryParams.class.getDeclaredMethod ("recordsByNameAndType", int.class, String.class, String.class)); - assertNull(md.template().body()); - assertNull(md.template().bodyTemplate()); - assertTrue(md.template().headers().isEmpty()); - assertEquals("/domains/{domainId}/records", md.template().url()); - assertEquals(Arrays.asList("{name}"), md.template().queries().get("name")); - assertEquals(Arrays.asList("{type}"), md.template().queries().get("type")); - assertEquals(Arrays.asList("domainId"), md.indexToName().get(0)); - assertEquals(Arrays.asList("name"), md.indexToName().get(1)); - assertEquals(Arrays.asList("type"), md.indexToName().get(2)); - assertEquals("GET /domains/{domainId}/records?name={name}&type={type} HTTP/1.1\n", md.template().toString()); + + assertThat(md.template()) + .hasQueries(entry("name", asList("{name}")), entry("type", asList("{type}"))); + + assertThat(md.indexToName()).containsExactly(entry(0, asList("domainId")), + entry(1, asList("name")), entry(2, asList("type"))); } @Test public void emptyQueryParam() throws Exception { thrown.expect(IllegalStateException.class); thrown.expectMessage("QueryParam.value() was empty on parameter 0"); - contract.parseAndValidatateMetadata(WithPathAndQueryParams.class.getDeclaredMethod("emptyQueryParam", String.class)); + contract.parseAndValidatateMetadata(WithPathAndQueryParams.class.getDeclaredMethod("empty", String.class)); } interface FormParams { @@ -330,12 +323,14 @@ interface FormParams { MethodMetadata md = contract.parseAndValidatateMetadata(FormParams.class.getDeclaredMethod("login", String.class, String.class, String.class)); - assertNull(md.template().body()); - assertNull(md.template().bodyTemplate()); - assertEquals(Arrays.asList("customer_name", "user_name", "password"), md.formParams()); - assertEquals(Arrays.asList("customer_name"), md.indexToName().get(0)); - assertEquals(Arrays.asList("user_name"), md.indexToName().get(1)); - assertEquals(Arrays.asList("password"), md.indexToName().get(2)); + assertThat(md.formParams()) + .containsExactly("customer_name", "user_name", "password"); + + assertThat(md.indexToName()).containsExactly( + entry(0, asList("customer_name")), + entry(1, asList("user_name")), + entry(2, asList("password")) + ); } @Test public void emptyFormParam() throws Exception { @@ -352,10 +347,14 @@ interface HeaderParams { } @Test public void headerParamsParseIntoIndexToName() throws Exception { - MethodMetadata md = contract.parseAndValidatateMetadata(HeaderParams.class.getDeclaredMethod("logout", String.class)); + MethodMetadata md = + contract.parseAndValidatateMetadata(HeaderParams.class.getDeclaredMethod("logout", String.class)); - assertEquals(Arrays.asList("{Auth-Token}"), md.template().headers().get("Auth-Token")); - assertEquals(Arrays.asList("Auth-Token"), md.indexToName().get(0)); + assertThat(md.template()) + .hasHeaders(entry("Auth-Token", asList("{Auth-Token}"))); + + assertThat(md.indexToName()) + .containsExactly(entry(0, asList("Auth-Token"))); } @Test public void emptyHeaderParam() throws Exception { @@ -371,8 +370,8 @@ interface PathsWithoutAnySlashes { } @Test public void pathsWithoutSlashesParseCorrectly() throws Exception { - MethodMetadata md = contract.parseAndValidatateMetadata(PathsWithoutAnySlashes.class.getDeclaredMethod("get")); - assertEquals("/base/specific", md.template().url()); + assertThat(contract.parseAndValidatateMetadata(PathsWithoutAnySlashes.class.getDeclaredMethod("get")).template()) + .hasUrl("/base/specific"); } @Path("/base") @@ -381,8 +380,8 @@ interface PathsWithSomeSlashes { } @Test public void pathsWithSomeSlashesParseCorrectly() throws Exception { - MethodMetadata md = contract.parseAndValidatateMetadata(PathsWithSomeSlashes.class.getDeclaredMethod("get")); - assertEquals("/base/specific", md.template().url()); + assertThat(contract.parseAndValidatateMetadata(PathsWithSomeSlashes.class.getDeclaredMethod("get")).template()) + .hasUrl("/base/specific"); } @Path("base") @@ -391,7 +390,8 @@ interface PathsWithSomeOtherSlashes { } @Test public void pathsWithSomeOtherSlashesParseCorrectly() throws Exception { - MethodMetadata md = contract.parseAndValidatateMetadata(PathsWithSomeOtherSlashes.class.getDeclaredMethod("get")); - assertEquals("/base/specific", md.template().url()); + assertThat(contract.parseAndValidatateMetadata(PathsWithSomeOtherSlashes.class.getDeclaredMethod("get")).template()) + .hasUrl("/base/specific"); + } } diff --git a/ribbon/build.gradle b/ribbon/build.gradle index 862cae5e06..05b2c6b73f 100644 --- a/ribbon/build.gradle +++ b/ribbon/build.gradle @@ -6,5 +6,6 @@ dependencies { compile project(':feign-core') compile 'com.netflix.ribbon:ribbon-loadbalancer:2.0-RC5' testCompile 'junit:junit:4.12' + testCompile 'org.assertj:assertj-core:1.7.1' testCompile 'com.squareup.okhttp:mockwebserver:2.2.0' } diff --git a/ribbon/src/main/java/feign/ribbon/RibbonClient.java b/ribbon/src/main/java/feign/ribbon/RibbonClient.java index cfa74cfe5d..1535c24fdc 100644 --- a/ribbon/src/main/java/feign/ribbon/RibbonClient.java +++ b/ribbon/src/main/java/feign/ribbon/RibbonClient.java @@ -1,6 +1,5 @@ package feign.ribbon; -import com.google.common.base.Throwables; import com.netflix.client.ClientException; import com.netflix.client.ClientFactory; import com.netflix.client.config.IClientConfig; @@ -61,7 +60,7 @@ public RibbonClient(Client delegate) { if (e.getCause() instanceof IOException) { throw IOException.class.cast(e.getCause()); } - throw Throwables.propagate(e); + throw new RuntimeException(e); } } diff --git a/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java b/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java index d447116927..ca15b9d93a 100644 --- a/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java +++ b/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java @@ -31,10 +31,13 @@ import static org.junit.Assert.assertEquals; import javax.inject.Named; +import org.junit.After; import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TestName; public class RibbonClientTest { + @Rule public final TestName testName = new TestName(); @Rule public final MockWebServerRule server1 = new MockWebServerRule(); @Rule public final MockWebServerRule server2 = new MockWebServerRule(); @@ -54,52 +57,37 @@ static class Module { } } - @Test - public void loadBalancingDefaultPolicyRoundRobin() throws IOException, InterruptedException { - String client = "RibbonClientTest-loadBalancingDefaultPolicyRoundRobin"; - String serverListKey = client + ".ribbon.listOfServers"; - + @Test public void loadBalancingDefaultPolicyRoundRobin() throws IOException, InterruptedException { server1.enqueue(new MockResponse().setBody("success!")); server2.enqueue(new MockResponse().setBody("success!")); - getConfigInstance().setProperty(serverListKey, hostAndPort(server1.getUrl("")) + "," + hostAndPort(server2.getUrl(""))); + getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.getUrl("")) + "," + hostAndPort(server2.getUrl(""))); - try { - TestInterface api = Feign.create(TestInterface.class, "http://" + client, new TestInterface.Module(), new RibbonModule()); + TestInterface api = Feign.create(TestInterface.class, "http://" + client(), new TestInterface.Module(), new RibbonModule()); - api.post(); - api.post(); + api.post(); + api.post(); - assertEquals(1, server1.getRequestCount()); - assertEquals(1, server2.getRequestCount()); - // TODO: verify ribbon stats match - // assertEquals(target.lb().getLoadBalancerStats().getSingleServerStat()) - } finally { - getConfigInstance().clearProperty(serverListKey); - } + assertEquals(1, server1.getRequestCount()); + assertEquals(1, server2.getRequestCount()); + // TODO: verify ribbon stats match + // assertEquals(target.lb().getLoadBalancerStats().getSingleServerStat()) } - @Test - public void ioExceptionRetry() throws IOException, InterruptedException { - String client = "RibbonClientTest-ioExceptionRetry"; - String serverListKey = client + ".ribbon.listOfServers"; - + @Test public void ioExceptionRetry() throws IOException, InterruptedException { server1.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); server1.enqueue(new MockResponse().setBody("success!")); - getConfigInstance().setProperty(serverListKey, hostAndPort(server1.getUrl(""))); + getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.getUrl(""))); - try { - TestInterface api = Feign.create(TestInterface.class, "http://" + client, new TestInterface.Module(), new RibbonModule()); - api.post(); + TestInterface api = Feign.create(TestInterface.class, "http://" + client(), new TestInterface.Module(), new RibbonModule()); - assertEquals(2, server1.getRequestCount()); - // TODO: verify ribbon stats match - // assertEquals(target.lb().getLoadBalancerStats().getSingleServerStat()) - } finally { - getConfigInstance().clearProperty(serverListKey); - } + api.post(); + + assertEquals(2, server1.getRequestCount()); + // TODO: verify ribbon stats match + // assertEquals(target.lb().getLoadBalancerStats().getSingleServerStat()) } /* @@ -109,61 +97,54 @@ public void ioExceptionRetry() throws IOException, InterruptedException { invalid characters (ex. space). */ @Test public void urlEncodeQueryStringParameters () throws IOException, InterruptedException { - String client = "RibbonClientTest-urlEncodeQueryStringParameters"; - String serverListKey = client + ".ribbon.listOfServers"; - String queryStringValue = "some string with space"; String expectedQueryStringValue = "some+string+with+space"; String expectedRequestLine = String.format("GET /?a=%s HTTP/1.1", expectedQueryStringValue); server1.enqueue(new MockResponse().setBody("success!")); - getConfigInstance().setProperty(serverListKey, hostAndPort(server1.getUrl(""))); + getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.getUrl(""))); - try { + TestInterface api = Feign.create(TestInterface.class, "http://" + client(), new TestInterface.Module(), new RibbonModule()); - TestInterface api = Feign.create(TestInterface.class, "http://" + client, new TestInterface.Module(), new RibbonModule()); + api.getWithQueryParameters(queryStringValue); - api.getWithQueryParameters(queryStringValue); + final String recordedRequestLine = server1.takeRequest().getRequestLine(); - final String recordedRequestLine = server1.takeRequest().getRequestLine(); - - assertEquals(recordedRequestLine, expectedRequestLine); - } finally { - getConfigInstance().clearProperty(serverListKey); - } + assertEquals(recordedRequestLine, expectedRequestLine); } + @Test public void ioExceptionRetryWithBuilder() throws IOException, InterruptedException { + server1.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); + server1.enqueue(new MockResponse().setBody("success!")); - @Test - public void ioExceptionRetryWithBuilder() throws IOException, InterruptedException { - String client = "RibbonClientTest-ioExceptionRetryWithBuilder"; - String serverListKey = client + ".ribbon.listOfServers"; - - server1.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); - server1.enqueue(new MockResponse().setBody("success!")); - - getConfigInstance().setProperty(serverListKey, hostAndPort(server1.getUrl(""))); - - try { - - TestInterface api = Feign.builder(). - client(new RibbonClient()). - target(TestInterface.class, "http://" + client); + getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.getUrl(""))); - api.post(); + TestInterface api = Feign.builder(). + client(new RibbonClient()). + target(TestInterface.class, "http://" + client()); - assertEquals(server1.getRequestCount(), 2); - // TODO: verify ribbon stats match - // assertEquals(target.lb().getLoadBalancerStats().getSingleServerStat()) - } finally { - getConfigInstance().clearProperty(serverListKey); - } - } + api.post(); + assertEquals(server1.getRequestCount(), 2); + // TODO: verify ribbon stats match + // assertEquals(target.lb().getLoadBalancerStats().getSingleServerStat()) + } static String hostAndPort(URL url) { // our build slaves have underscores in their hostnames which aren't permitted by ribbon return "localhost:" + url.getPort(); } + + private String client() { + return testName.getMethodName(); + } + + private String serverListKey() { + return client() + ".ribbon.listOfServers"; + } + + @After public void clearServerList() { + getConfigInstance().clearProperty(serverListKey()); + } } diff --git a/sax/build.gradle b/sax/build.gradle index b50c180267..7d9b05dbc1 100644 --- a/sax/build.gradle +++ b/sax/build.gradle @@ -4,6 +4,6 @@ sourceCompatibility = 1.6 dependencies { compile project(':feign-core') - testCompile 'com.google.guava:guava:14.0.1' testCompile 'junit:junit:4.12' + testCompile 'org.assertj:assertj-core:1.7.1' } diff --git a/sax/src/test/java/feign/sax/examples/AWSSignatureVersion4.java b/sax/src/test/java/feign/sax/examples/AWSSignatureVersion4.java index c229587b8c..53b2671f92 100644 --- a/sax/src/test/java/feign/sax/examples/AWSSignatureVersion4.java +++ b/sax/src/test/java/feign/sax/examples/AWSSignatureVersion4.java @@ -15,32 +15,20 @@ */ package feign.sax.examples; -import com.google.common.base.Function; -import com.google.common.base.Joiner; -import com.google.common.collect.Multimap; -import com.google.common.collect.TreeMultimap; - +import feign.Request; +import feign.RequestTemplate; import java.net.URI; +import java.security.MessageDigest; import java.text.SimpleDateFormat; -import java.util.Collection; import java.util.Date; -import java.util.Map.Entry; import java.util.TimeZone; - import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; -import feign.Request; -import feign.RequestTemplate; - -import static com.google.common.base.Throwables.propagate; -import static com.google.common.collect.Iterables.transform; -import static com.google.common.hash.Hashing.sha256; -import static com.google.common.io.BaseEncoding.base16; import static feign.Util.UTF_8; // http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html -public class AWSSignatureVersion4 implements Function { +public class AWSSignatureVersion4 { String region = "us-east-1"; String service = "iam"; @@ -52,31 +40,30 @@ public AWSSignatureVersion4(String accessKey, String secretKey) { this.secretKey = secretKey; } - @Override public Request apply(RequestTemplate input) { - input.header("Host", URI.create(input.url()).getHost()); - TreeMultimap sortedLowercaseHeaders = TreeMultimap.create(); - for (String key : input.headers().keySet()) { - sortedLowercaseHeaders.putAll(trimToLowercase.apply(key), - transform(input.headers().get(key), trimToLowercase)); - } + public Request apply(RequestTemplate input) { + if (!input.headers().isEmpty()) throw new UnsupportedOperationException("headers not supported"); + if (input.body() != null) throw new UnsupportedOperationException("body not supported"); + + String host = URI.create(input.url()).getHost(); String timestamp; synchronized (iso8601) { timestamp = iso8601.format(new Date()); } - String credentialScope = Joiner.on('/').join(timestamp.substring(0, 8), region, service, "aws4_request"); + String credentialScope = String.format("%s/%s/%s/%s", timestamp.substring(0, 8), region, service, "aws4_request"); input.query("X-Amz-Algorithm", "AWS4-HMAC-SHA256"); input.query("X-Amz-Credential", accessKey + "/" + credentialScope); input.query("X-Amz-Date", timestamp); - input.query("X-Amz-SignedHeaders", Joiner.on(';').join(sortedLowercaseHeaders.keySet())); + input.query("X-Amz-SignedHeaders", "host"); + input.header("Host", host); - String canonicalString = canonicalString(input, sortedLowercaseHeaders); + String canonicalString = canonicalString(input, host); String toSign = toSign(timestamp, credentialScope, canonicalString); byte[] signatureKey = signatureKey(secretKey, timestamp); - String signature = base16().lowerCase().encode(hmacSHA256(toSign, signatureKey)); + String signature = hex(hmacSHA256(toSign, signatureKey)); input.query("X-Amz-Signature", signature); @@ -99,13 +86,13 @@ static byte[] hmacSHA256(String data, byte[] key) { mac.init(new SecretKeySpec(key, algorithm)); return mac.doFinal(data.getBytes(UTF_8)); } catch (Exception e) { - throw propagate(e); + throw new RuntimeException(e); } } private static final String EMPTY_STRING_HASH = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; - private String canonicalString(RequestTemplate input, Multimap sortedLowercaseHeaders) { + private static String canonicalString(RequestTemplate input, String host) { StringBuilder canonicalRequest = new StringBuilder(); // HTTPRequestMethod + '\n' + canonicalRequest.append(input.method()).append('\n'); @@ -118,33 +105,25 @@ private String canonicalString(RequestTemplate input, Multimap s canonicalRequest.append('\n'); // CanonicalHeaders + '\n' + - for (Entry> entry : sortedLowercaseHeaders.asMap().entrySet()) { - canonicalRequest.append(entry.getKey()).append(':').append(Joiner.on(',').join(entry.getValue())) - .append('\n'); - } + canonicalRequest.append("host:").append(host).append('\n'); + canonicalRequest.append('\n'); // SignedHeaders + '\n' + - canonicalRequest.append(Joiner.on(',').join(sortedLowercaseHeaders.keySet())).append('\n'); + canonicalRequest.append("host").append('\n'); // HexEncode(Hash(Payload)) String bodyText = input.charset() != null && input.body() != null ? new String(input.body(), input.charset()) : null; if (bodyText != null) { - canonicalRequest.append(base16().lowerCase().encode(sha256().hashString(bodyText, UTF_8).asBytes())); + canonicalRequest.append(hex(sha256(bodyText))); } else { canonicalRequest.append(EMPTY_STRING_HASH); } return canonicalRequest.toString(); } - private static final Function trimToLowercase = new Function() { - public String apply(String in) { - return in == null ? null : in.toLowerCase().trim(); - } - }; - - private String toSign(String timestamp, String credentialScope, String canonicalRequest) { + private static String toSign(String timestamp, String credentialScope, String canonicalRequest) { StringBuilder toSign = new StringBuilder(); // Algorithm + '\n' + toSign.append("AWS4-HMAC-SHA256").append('\n'); @@ -153,10 +132,28 @@ private String toSign(String timestamp, String credentialScope, String canonical // CredentialScope + '\n' + toSign.append(credentialScope).append('\n'); // HexEncode(Hash(CanonicalRequest)) - toSign.append(base16().lowerCase().encode(sha256().hashString(canonicalRequest, UTF_8).asBytes())); + toSign.append(hex(sha256(canonicalRequest))); return toSign.toString(); } + + private static String hex(byte[] data) { + StringBuilder result = new StringBuilder(data.length * 2); + for (byte b : data) { + result.append(String.format("%02x", b & 0xff)); + } + return result.toString(); + } + + static byte[] sha256(String data) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + return digest.digest(data.getBytes(UTF_8)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + private static final SimpleDateFormat iso8601 = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"); static { diff --git a/slf4j/build.gradle b/slf4j/build.gradle index 144e040043..07c7fc78ca 100644 --- a/slf4j/build.gradle +++ b/slf4j/build.gradle @@ -4,5 +4,6 @@ dependencies { compile project(':feign-core') compile 'org.slf4j:slf4j-api:1.7.5' testCompile 'junit:junit:4.12' + testCompile 'org.assertj:assertj-core:1.7.1' testCompile 'org.slf4j:slf4j-simple:1.7.5' } From 067997912e0f53420b05cbfca77424a75b68d91a Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Sun, 25 Jan 2015 20:47:50 -0800 Subject: [PATCH 156/179] Introduces feign.@Param to annotate template parameters Feign 8.x will no longer support Dagger, nor interfaces annotated with `javax.inject.@Named`. Users must migrate from `javax.inject.@Named` to `feign.@Param` via Feign v7.1+ before attempting to update to Feign 8.0. For example, the following uses `@Param` as opposed to `@Named` to annotate template parameters. ```java interface GitHub { @RequestLine("GET /repos/{owner}/{repo}/contributors") List contributors(@Param("owner") String owner, @Param("repo") String repo); } ``` --- CHANGELOG.md | 3 + README.md | 4 +- core/src/main/java/feign/Body.java | 2 +- core/src/main/java/feign/Contract.java | 11 +-- core/src/main/java/feign/Headers.java | 2 +- core/src/main/java/feign/Param.java | 28 +++++++ core/src/main/java/feign/RequestLine.java | 4 +- core/src/main/java/feign/codec/Encoder.java | 2 +- .../test/java/feign/DefaultContractTest.java | 78 +++++++++++++++++-- core/src/test/java/feign/FeignTest.java | 36 ++++----- core/src/test/java/feign/LoggerTest.java | 5 +- .../java/feign/assertj/FeignAssertions.java | 15 ++++ .../assertj/MockWebServerAssertions.java | 15 ++++ .../feign/assertj/RecordedRequestAssert.java | 16 +++- .../feign/assertj/RequestTemplateAssert.java | 16 +++- .../java/feign/examples/GitHubExample.java | 4 +- .../feign/gson/examples/GitHubExample.java | 4 +- .../feign/jackson/examples/GitHubExample.java | 4 +- .../main/java/feign/ribbon/RibbonModule.java | 9 +-- .../java/feign/ribbon/RibbonClientTest.java | 13 ++-- 20 files changed, 207 insertions(+), 64 deletions(-) create mode 100644 core/src/main/java/feign/Param.java diff --git a/CHANGELOG.md b/CHANGELOG.md index f0bc1b79df..b0aa906004 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +### Version 7.1 +* Introduces feign.@Param to annotate template parameters. Users must migrate from `javax.inject.@Named` to `feign.@Param` before updating to Feign 8.0. + ### Version 7.0 * Expose reflective dispatch hook: InvocationHandlerFactory * Add JAXB integration diff --git a/README.md b/README.md index 19e501064f..2aec15053e 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Usage typically looks like this, an adaptation of the [canonical Retrofit sample ```java interface GitHub { @RequestLine("GET /repos/{owner}/{repo}/contributors") - List contributors(@Named("owner") String owner, @Named("repo") String repo); + List contributors(@Param("owner") String owner, @Param("repo") String repo); } static class Contributor { @@ -44,7 +44,7 @@ Feign has several aspects that can be customized. For simple cases, you can use ```java interface Bank { @RequestLine("POST /account/{id}") - Account getAccountInfo(@Named("id") String id); + Account getAccountInfo(@Param("id") String id); } ... Bank bank = Feign.builder().decoder(new AccountDecoder()).target(Bank.class, "https://api.examplebank.com"); diff --git a/core/src/main/java/feign/Body.java b/core/src/main/java/feign/Body.java index f4d5d2bdc9..9c3e094ed0 100644 --- a/core/src/main/java/feign/Body.java +++ b/core/src/main/java/feign/Body.java @@ -15,7 +15,7 @@ *
*
  * @Body("<v01:getResourceRecordsOfZone><zoneName>{zoneName}</zoneName><rrType>0</rrType></v01:getResourceRecordsOfZone>")
- * List<Record> listByZone(@Named("zoneName") String zoneName);
+ * List<Record> listByZone(@Param("zoneName") String zoneName);
  * 
*
* Note that if you'd like curly braces literally in the body, urlencode diff --git a/core/src/main/java/feign/Contract.java b/core/src/main/java/feign/Contract.java index d9ac3bd110..400a5478cd 100644 --- a/core/src/main/java/feign/Contract.java +++ b/core/src/main/java/feign/Contract.java @@ -159,11 +159,12 @@ protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodA @Override protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) { boolean isHttpAnnotation = false; - for (Annotation parameterAnnotation : annotations) { - Class annotationType = parameterAnnotation.annotationType(); - if (annotationType == Named.class) { - String name = Named.class.cast(parameterAnnotation).value(); - checkState(emptyToNull(name) != null, "Named annotation was empty on param %s.", paramIndex); + for (Annotation annotation : annotations) { + Class annotationType = annotation.annotationType(); + if (annotationType == Param.class || annotationType == Named.class) { + String name = annotationType == Param.class ? ((Param) annotation).value() : ((Named) annotation).value(); + checkState(emptyToNull(name) != null, + "%s annotation was empty on param %s.", annotationType.getSimpleName(), paramIndex); nameParam(data, name, paramIndex); isHttpAnnotation = true; String varName = '{' + name + '}'; diff --git a/core/src/main/java/feign/Headers.java b/core/src/main/java/feign/Headers.java index b1d7061fe1..b250fb65fa 100644 --- a/core/src/main/java/feign/Headers.java +++ b/core/src/main/java/feign/Headers.java @@ -18,7 +18,7 @@ * @Headers({ * "X-Foo: Bar", * "X-Ping: {token}" - * }) void post(@Named("token") String token); + * }) void post(@Param("token") String token); * ... *
*
diff --git a/core/src/main/java/feign/Param.java b/core/src/main/java/feign/Param.java new file mode 100644 index 0000000000..d62e4decba --- /dev/null +++ b/core/src/main/java/feign/Param.java @@ -0,0 +1,28 @@ +/* + * Copyright 2015 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.lang.annotation.Retention; + +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** The name of a template variable applied to {@link Headers}, {@linkplain RequestLine} or {@linkplain Body} */ +@Retention(RUNTIME) +@java.lang.annotation.Target(PARAMETER) +public @interface Param { + String value(); +} diff --git a/core/src/main/java/feign/RequestLine.java b/core/src/main/java/feign/RequestLine.java index b344144c53..14c1d68005 100644 --- a/core/src/main/java/feign/RequestLine.java +++ b/core/src/main/java/feign/RequestLine.java @@ -15,7 +15,7 @@ * ... * * @RequestLine("GET /servers/{serverId}?count={count}") - * void get(@Named("serverId") String serverId, @Named("count") int count); + * void get(@Param("serverId") String serverId, @Param("count") int count); * ... * * @RequestLine("GET") @@ -39,7 +39,7 @@ * Feign: *
  * @RequestLine("GET /servers/{serverId}?count={count}")
- * void get(@Named("serverId") String serverId, @Named("count") int count);
+ * void get(@Param("serverId") String serverId, @Param("count") int count);
  * ...
  * 
*
diff --git a/core/src/main/java/feign/codec/Encoder.java b/core/src/main/java/feign/codec/Encoder.java index c3b07d591a..b743d3423f 100644 --- a/core/src/main/java/feign/codec/Encoder.java +++ b/core/src/main/java/feign/codec/Encoder.java @@ -55,7 +55,7 @@ *
  * @POST
  * @Path("/")
- * Session login(@Named("username") String username, @Named("password") String password);
+ * Session login(@Param("username") String username, @Param("password") String password);
  * 
*/ public interface Encoder { diff --git a/core/src/test/java/feign/DefaultContractTest.java b/core/src/test/java/feign/DefaultContractTest.java index 404f9f5533..826f91c69a 100644 --- a/core/src/test/java/feign/DefaultContractTest.java +++ b/core/src/test/java/feign/DefaultContractTest.java @@ -164,7 +164,7 @@ interface BodyWithoutParameters { } interface WithURIParam { - @RequestLine("GET /{1}/{2}") Response uriParam(@Named("1") String one, URI endpoint, @Named("2") String two); + @RequestLine("GET /{1}/{2}") Response uriParam(@Param("1") String one, URI endpoint, @Param("2") String two); } @Test public void withPathAndURIParam() throws Exception { @@ -183,8 +183,8 @@ interface WithURIParam { interface WithPathAndQueryParams { @RequestLine("GET /domains/{domainId}/records?name={name}&type={type}") - Response recordsByNameAndType(@Named("domainId") int id, @Named("name") String nameFilter, - @Named("type") String typeFilter); + Response recordsByNameAndType(@Param("domainId") int id, @Param("name") String nameFilter, + @Param("type") String typeFilter); } @Test public void pathAndQueryParams() throws Exception { @@ -205,8 +205,8 @@ interface FormParams { @RequestLine("POST /") @Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D") void login( - @Named("customer_name") String customer, - @Named("user_name") String user, @Named("password") String password); + @Param("customer_name") String customer, + @Param("user_name") String user, @Param("password") String password); } @Test public void bodyWithTemplate() throws Exception { @@ -233,7 +233,7 @@ void login( interface HeaderParams { @RequestLine("POST /") - @Headers("Auth-Token: {Auth-Token}") void logout(@Named("Auth-Token") String token); + @Headers("Auth-Token: {Auth-Token}") void logout(@Param("Auth-Token") String token); } @Test public void headerParamsParseIntoIndexToName() throws Exception { @@ -244,4 +244,70 @@ interface HeaderParams { assertThat(md.indexToName()) .containsExactly(entry(0, asList("Auth-Token"))); } + + // TODO: remove all of below in 8.x + + interface WithPathAndQueryParamsAnnotatedWithNamed { + @RequestLine("GET /domains/{domainId}/records?name={name}&type={type}") + Response recordsByNameAndType(@Named("domainId") int id, @Named("name") String nameFilter, + @Named("type") String typeFilter); + } + + @Test public void pathAndQueryParamsAnnotatedWithNamed() throws Exception { + MethodMetadata md = contract.parseAndValidatateMetadata(WithPathAndQueryParamsAnnotatedWithNamed.class.getDeclaredMethod + ("recordsByNameAndType", int.class, String.class, String.class)); + + assertThat(md.template()) + .hasQueries(entry("name", asList("{name}")), entry("type", asList("{type}"))); + + assertThat(md.indexToName()).containsExactly( + entry(0, asList("domainId")), + entry(1, asList("name")), + entry(2, asList("type")) + ); + } + + interface FormParamsAnnotatedWithNamed { + @RequestLine("POST /") + @Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D") + void login( + @Named("customer_name") String customer, + @Named("user_name") String user, @Named("password") String password); + } + + @Test public void bodyWithTemplateAnnotatedWithNamed() throws Exception { + MethodMetadata md = contract.parseAndValidatateMetadata(FormParamsAnnotatedWithNamed.class.getDeclaredMethod("login", String.class, + String.class, String.class)); + + assertThat(md.template()) + .hasBodyTemplate("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D"); + } + + @Test public void formParamsAnnotatedWithNamedParseIntoIndexToName() throws Exception { + MethodMetadata md = contract.parseAndValidatateMetadata(FormParamsAnnotatedWithNamed.class.getDeclaredMethod("login", String.class, + String.class, String.class)); + + assertThat(md.formParams()) + .containsExactly("customer_name", "user_name", "password"); + + assertThat(md.indexToName()).containsExactly( + entry(0, asList("customer_name")), + entry(1, asList("user_name")), + entry(2, asList("password")) + ); + } + + interface HeaderParamsAnnotatedWithNamed { + @RequestLine("POST /") + @Headers("Auth-Token: {Auth-Token}") void logout(@Named("Auth-Token") String token); + } + + @Test public void headerParamsAnnotatedWithNamedParseIntoIndexToName() throws Exception { + MethodMetadata md = contract.parseAndValidatateMetadata(HeaderParamsAnnotatedWithNamed.class.getDeclaredMethod("logout", String.class)); + + assertThat(md.template()).hasHeaders(entry("Auth-Token", asList("{Auth-Token}"))); + + assertThat(md.indexToName()) + .containsExactly(entry(0, asList("Auth-Token"))); + } } diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index b409ea9093..8d7aee5182 100644 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -33,8 +33,6 @@ import java.util.Arrays; import java.util.List; import java.util.Map; -import java.util.concurrent.Executor; -import javax.inject.Named; import javax.inject.Singleton; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLSession; @@ -63,18 +61,18 @@ interface TestInterface { @RequestLine("POST /") @Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D") void login( - @Named("customer_name") String customer, @Named("user_name") String user, @Named("password") String password); + @Param("customer_name") String customer, @Param("user_name") String user, @Param("password") String password); @RequestLine("POST /") void body(List contents); @RequestLine("POST /") @Headers("Content-Encoding: gzip") void gzipBody(List contents); @RequestLine("POST /") void form( - @Named("customer_name") String customer, @Named("user_name") String user, @Named("password") String password); + @Param("customer_name") String customer, @Param("user_name") String user, @Param("password") String password); - @RequestLine("GET /{1}/{2}") Response uriParam(@Named("1") String one, URI endpoint, @Named("2") String two); + @RequestLine("GET /{1}/{2}") Response uriParam(@Param("1") String one, URI endpoint, @Param("2") String two); - @RequestLine("GET /?1={1}&2={2}") Response queryParams(@Named("1") String one, @Named("2") Iterable twos); + @RequestLine("GET /?1={1}&2={2}") Response queryParams(@Param("1") String one, @Param("2") Iterable twos); @dagger.Module(injects = Feign.class, addsTo = Feign.Defaults.class) static class Module { @@ -116,22 +114,12 @@ interface OtherTestInterface { @RequestLine("POST /") void binaryRequestBody(byte[] contents); } - @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 postTemplateParamsResolve() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("foo")); - TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module()); + TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), + new TestInterface.Module()); api.login("netflix", "denominator", "password"); @@ -155,7 +143,8 @@ public void responseCoercesToStringBody() throws IOException, InterruptedExcepti public void postFormParams() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("foo")); - TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module()); + TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), + new TestInterface.Module()); api.form("netflix", "denominator", "password"); @@ -180,7 +169,8 @@ public void postBodyParam() throws IOException, InterruptedException { public void postGZIPEncodedBodyParam() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("foo")); - TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module()); + TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), + new TestInterface.Module()); api.gzipBody(Arrays.asList("netflix", "denominator", "password")); @@ -239,8 +229,8 @@ public void multipleInterceptor() throws IOException, InterruptedException { @Test public void toKeyMethodFormatsAsExpected() throws Exception { assertEquals("TestInterface#post()", Feign.configKey(TestInterface.class.getDeclaredMethod("post"))); - assertEquals("TestInterface#uriParam(String,URI,String)", Feign.configKey( - TestInterface.class.getDeclaredMethod("uriParam", String.class, URI.class, String.class))); + assertEquals("TestInterface#uriParam(String,URI,String)", + Feign.configKey(TestInterface.class.getDeclaredMethod("uriParam", String.class, URI.class, String.class))); } @dagger.Module(overrides = true, library = true, includes = TestInterface.Module.class) @@ -266,7 +256,7 @@ public void canOverrideErrorDecoder() throws IOException, InterruptedException { thrown.expectMessage("zone not found"); TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), - new IllegalArgumentExceptionOn404()); + new IllegalArgumentExceptionOn404()); api.post(); } diff --git a/core/src/test/java/feign/LoggerTest.java b/core/src/test/java/feign/LoggerTest.java index 57cc9ca1ae..8748fc279c 100644 --- a/core/src/test/java/feign/LoggerTest.java +++ b/core/src/test/java/feign/LoggerTest.java @@ -23,7 +23,6 @@ import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; -import javax.inject.Named; import org.assertj.core.api.SoftAssertions; import org.junit.Rule; import org.junit.Test; @@ -47,8 +46,8 @@ interface SendsStuff { @Headers("Content-Type: application/json") @Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D") String login( - @Named("customer_name") String customer, - @Named("user_name") String user, @Named("password") String password); + @Param("customer_name") String customer, + @Param("user_name") String user, @Param("password") String password); } @RunWith(Parameterized.class) diff --git a/core/src/test/java/feign/assertj/FeignAssertions.java b/core/src/test/java/feign/assertj/FeignAssertions.java index 821a80b851..bbd83d7c49 100644 --- a/core/src/test/java/feign/assertj/FeignAssertions.java +++ b/core/src/test/java/feign/assertj/FeignAssertions.java @@ -1,3 +1,18 @@ +/* + * Copyright 2015 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.assertj; import feign.RequestTemplate; diff --git a/core/src/test/java/feign/assertj/MockWebServerAssertions.java b/core/src/test/java/feign/assertj/MockWebServerAssertions.java index e6cbb1c146..cdb354581c 100644 --- a/core/src/test/java/feign/assertj/MockWebServerAssertions.java +++ b/core/src/test/java/feign/assertj/MockWebServerAssertions.java @@ -1,3 +1,18 @@ +/* + * Copyright 2015 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.assertj; import com.squareup.okhttp.mockwebserver.RecordedRequest; diff --git a/core/src/test/java/feign/assertj/RecordedRequestAssert.java b/core/src/test/java/feign/assertj/RecordedRequestAssert.java index 54a2c3a65f..fed0d93909 100644 --- a/core/src/test/java/feign/assertj/RecordedRequestAssert.java +++ b/core/src/test/java/feign/assertj/RecordedRequestAssert.java @@ -1,3 +1,18 @@ +/* + * Copyright 2015 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.assertj; import com.squareup.okhttp.mockwebserver.RecordedRequest; @@ -16,7 +31,6 @@ import static org.assertj.core.error.ShouldNotContain.shouldNotContain; public final class RecordedRequestAssert extends AbstractAssert { - ByteArrays arrays = ByteArrays.instance(); Objects objects = Objects.instance(); Iterables iterables = Iterables.instance(); diff --git a/core/src/test/java/feign/assertj/RequestTemplateAssert.java b/core/src/test/java/feign/assertj/RequestTemplateAssert.java index d2e9a26af6..8283222063 100644 --- a/core/src/test/java/feign/assertj/RequestTemplateAssert.java +++ b/core/src/test/java/feign/assertj/RequestTemplateAssert.java @@ -1,3 +1,18 @@ +/* + * Copyright 2015 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.assertj; import feign.RequestTemplate; @@ -10,7 +25,6 @@ import static feign.Util.UTF_8; public final class RequestTemplateAssert extends AbstractAssert { - ByteArrays arrays = ByteArrays.instance(); Objects objects = Objects.instance(); Maps maps = Maps.instance(); diff --git a/core/src/test/java/feign/examples/GitHubExample.java b/core/src/test/java/feign/examples/GitHubExample.java index c52308d52f..7b0d191020 100644 --- a/core/src/test/java/feign/examples/GitHubExample.java +++ b/core/src/test/java/feign/examples/GitHubExample.java @@ -19,11 +19,11 @@ import com.google.gson.JsonIOException; import feign.Feign; import feign.Logger; +import feign.Param; import feign.RequestLine; import feign.Response; import feign.codec.Decoder; -import javax.inject.Named; import java.io.IOException; import java.io.Reader; import java.lang.reflect.Type; @@ -38,7 +38,7 @@ public class GitHubExample { interface GitHub { @RequestLine("GET /repos/{owner}/{repo}/contributors") - List contributors(@Named("owner") String owner, @Named("repo") String repo); + List contributors(@Param("owner") String owner, @Param("repo") String repo); } static class Contributor { diff --git a/gson/src/test/java/feign/gson/examples/GitHubExample.java b/gson/src/test/java/feign/gson/examples/GitHubExample.java index 6053ce51a5..6d41f7007f 100644 --- a/gson/src/test/java/feign/gson/examples/GitHubExample.java +++ b/gson/src/test/java/feign/gson/examples/GitHubExample.java @@ -16,10 +16,10 @@ package feign.gson.examples; import feign.Feign; +import feign.Param; import feign.RequestLine; import feign.gson.GsonDecoder; -import javax.inject.Named; import java.util.List; /** @@ -29,7 +29,7 @@ public class GitHubExample { interface GitHub { @RequestLine("GET /repos/{owner}/{repo}/contributors") - List contributors(@Named("owner") String owner, @Named("repo") String repo); + List contributors(@Param("owner") String owner, @Param("repo") String repo); } static class Contributor { diff --git a/jackson/src/test/java/feign/jackson/examples/GitHubExample.java b/jackson/src/test/java/feign/jackson/examples/GitHubExample.java index 24f490efb3..73bacef4c3 100644 --- a/jackson/src/test/java/feign/jackson/examples/GitHubExample.java +++ b/jackson/src/test/java/feign/jackson/examples/GitHubExample.java @@ -1,10 +1,10 @@ package feign.jackson.examples; import feign.Feign; +import feign.Param; import feign.RequestLine; import feign.jackson.JacksonDecoder; -import javax.inject.Named; import java.util.List; /** @@ -13,7 +13,7 @@ public class GitHubExample { interface GitHub { @RequestLine("GET /repos/{owner}/{repo}/contributors") - List contributors(@Named("owner") String owner, @Named("repo") String repo); + List contributors(@Param("owner") String owner, @Param("repo") String repo); } static class Contributor { diff --git a/ribbon/src/main/java/feign/ribbon/RibbonModule.java b/ribbon/src/main/java/feign/ribbon/RibbonModule.java index fab62b970f..33ed6bc8e6 100644 --- a/ribbon/src/main/java/feign/ribbon/RibbonModule.java +++ b/ribbon/src/main/java/feign/ribbon/RibbonModule.java @@ -15,15 +15,10 @@ */ package feign.ribbon; -import java.io.IOException; -import java.net.URI; - -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; - import dagger.Provides; import feign.Client; +import javax.inject.Named; +import javax.inject.Singleton; /** * Adding this module will override URL resolution of {@link feign.Client Feign's client}, diff --git a/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java b/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java index ca15b9d93a..346a2ff139 100644 --- a/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java +++ b/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java @@ -20,6 +20,7 @@ import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule; import dagger.Provides; import feign.Feign; +import feign.Param; import feign.RequestLine; import feign.codec.Decoder; import feign.codec.Encoder; @@ -30,7 +31,6 @@ import static com.netflix.config.ConfigurationManager.getConfigInstance; import static org.junit.Assert.assertEquals; -import javax.inject.Named; import org.junit.After; import org.junit.Rule; import org.junit.Test; @@ -43,7 +43,7 @@ public class RibbonClientTest { interface TestInterface { @RequestLine("POST /") void post(); - @RequestLine("GET /?a={a}") void getWithQueryParameters(@Named("a") String a); + @RequestLine("GET /?a={a}") void getWithQueryParameters(@Param("a") String a); @dagger.Module(injects = Feign.class, overrides = true, addsTo = Feign.Defaults.class) static class Module { @@ -63,7 +63,8 @@ static class Module { getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.getUrl("")) + "," + hostAndPort(server2.getUrl(""))); - TestInterface api = Feign.create(TestInterface.class, "http://" + client(), new TestInterface.Module(), new RibbonModule()); + TestInterface api = Feign.create(TestInterface.class, "http://" + client(), new TestInterface.Module(), + new RibbonModule()); api.post(); api.post(); @@ -81,7 +82,8 @@ static class Module { getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.getUrl(""))); - TestInterface api = Feign.create(TestInterface.class, "http://" + client(), new TestInterface.Module(), new RibbonModule()); + TestInterface api = Feign.create(TestInterface.class, "http://" + client(), new TestInterface.Module(), + new RibbonModule()); api.post(); @@ -105,7 +107,8 @@ invalid characters (ex. space). getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.getUrl(""))); - TestInterface api = Feign.create(TestInterface.class, "http://" + client(), new TestInterface.Module(), new RibbonModule()); + TestInterface api = Feign.create(TestInterface.class, "http://" + client(), new TestInterface.Module(), + new RibbonModule()); api.getWithQueryParameters(queryStringValue); From 194d82fa5c103488f7f60da4f6d443d8b6e3d5c7 Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Mon, 26 Jan 2015 01:12:03 -0800 Subject: [PATCH 157/179] Allows multiple headers with the same name; Backfills default client tests. --- CHANGELOG.md | 1 + core/src/main/java/feign/Contract.java | 9 +- core/src/main/java/feign/RequestTemplate.java | 16 +- .../test/java/feign/DefaultContractTest.java | 6 +- core/src/test/java/feign/FeignTest.java | 76 +-------- .../java/feign/client/DefaultClientTest.java | 160 ++++++++++++++++++ .../TrustingSSLSocketFactory.java | 2 +- 7 files changed, 185 insertions(+), 85 deletions(-) create mode 100644 core/src/test/java/feign/client/DefaultClientTest.java rename core/src/test/java/feign/{ => client}/TrustingSSLSocketFactory.java (99%) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0aa906004..f898bd8d63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ### Version 7.1 * Introduces feign.@Param to annotate template parameters. Users must migrate from `javax.inject.@Named` to `feign.@Param` before updating to Feign 8.0. +* Allows multiple headers with the same name. ### Version 7.0 * Expose reflective dispatch hook: InvocationHandlerFactory diff --git a/core/src/main/java/feign/Contract.java b/core/src/main/java/feign/Contract.java index 400a5478cd..2001fa9427 100644 --- a/core/src/main/java/feign/Contract.java +++ b/core/src/main/java/feign/Contract.java @@ -15,6 +15,7 @@ */ package feign; +import java.util.LinkedHashMap; import javax.inject.Named; import java.lang.annotation.Annotation; import java.lang.reflect.Method; @@ -149,10 +150,16 @@ protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodA } else if (annotationType == Headers.class) { String[] headersToParse = Headers.class.cast(methodAnnotation).value(); checkState(headersToParse.length > 0, "Headers annotation was empty on method %s.", method.getName()); + Map> headers = new LinkedHashMap>(headersToParse.length); for (String header : headersToParse) { int colon = header.indexOf(':'); - data.template().header(header.substring(0, colon), header.substring(colon + 2)); + String name = header.substring(0, colon); + if (!headers.containsKey(name)) { + headers.put(name, new ArrayList(1)); + } + headers.get(name).add(header.substring(colon + 2)); } + data.template().headers(headers); } } diff --git a/core/src/main/java/feign/RequestTemplate.java b/core/src/main/java/feign/RequestTemplate.java index 42c6b9046e..8dc652cbe4 100644 --- a/core/src/main/java/feign/RequestTemplate.java +++ b/core/src/main/java/feign/RequestTemplate.java @@ -332,28 +332,28 @@ public Map> queries() { * template.query("X-Application-Version", "{version}"); * * - * @param configKey the configKey of the header + * @param name the name of the header * @param values can be a single null to imply removing all values. Else no * values are expected to be null. * @see #headers() */ - public RequestTemplate header(String configKey, String... values) { - checkNotNull(configKey, "header configKey"); + public RequestTemplate header(String name, String... values) { + checkNotNull(name, "header name"); if (values == null || (values.length == 1 && values[0] == null)) { - headers.remove(configKey); + headers.remove(name); } else { List headers = new ArrayList(); headers.addAll(Arrays.asList(values)); - this.headers.put(configKey, headers); + this.headers.put(name, headers); } return this; } /* @see #header(String, String...) */ - public RequestTemplate header(String configKey, Iterable values) { + public RequestTemplate header(String name, Iterable values) { if (values != null) - return header(configKey, toArray(values, String.class)); - return header(configKey, (String[]) null); + return header(name, toArray(values, String.class)); + return header(name, (String[]) null); } /** diff --git a/core/src/test/java/feign/DefaultContractTest.java b/core/src/test/java/feign/DefaultContractTest.java index 826f91c69a..495c7f03fa 100644 --- a/core/src/test/java/feign/DefaultContractTest.java +++ b/core/src/test/java/feign/DefaultContractTest.java @@ -233,13 +233,15 @@ void login( interface HeaderParams { @RequestLine("POST /") - @Headers("Auth-Token: {Auth-Token}") void logout(@Param("Auth-Token") String token); + @Headers({"Auth-Token: {Auth-Token}", "Auth-Token: Foo"}) + void logout(@Param("Auth-Token") String token); } @Test public void headerParamsParseIntoIndexToName() throws Exception { MethodMetadata md = contract.parseAndValidatateMetadata(HeaderParams.class.getDeclaredMethod("logout", String.class)); - assertThat(md.template()).hasHeaders(entry("Auth-Token", asList("{Auth-Token}"))); + assertThat(md.template()) + .hasHeaders(entry("Auth-Token", asList("{Auth-Token}", "Foo"))); assertThat(md.indexToName()) .containsExactly(entry(0, asList("Auth-Token"))); diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index 8d7aee5182..9f29831ed7 100644 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -17,7 +17,6 @@ import com.google.gson.Gson; import com.squareup.okhttp.mockwebserver.MockResponse; -import com.squareup.okhttp.mockwebserver.MockWebServer; import com.squareup.okhttp.mockwebserver.SocketPolicy; import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule; import dagger.Module; @@ -34,9 +33,6 @@ import java.util.List; import java.util.Map; import javax.inject.Singleton; -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.SSLSession; -import javax.net.ssl.SSLSocketFactory; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -156,7 +152,8 @@ public void postFormParams() throws IOException, InterruptedException { public void postBodyParam() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("foo")); - TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module()); + TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), + new TestInterface.Module()); api.body(Arrays.asList("netflix", "denominator", "password")); @@ -341,8 +338,7 @@ public void doesntRetryAfterResponseIsSent() throws IOException, InterruptedExce thrown.expect(FeignException.class); thrown.expectMessage("error reading response POST http://"); - TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), - new IOEOnDecode()); + TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new IOEOnDecode()); try { api.post(); @@ -351,72 +347,6 @@ public void doesntRetryAfterResponseIsSent() throws IOException, InterruptedExce } } - @Module(overrides = true, includes = TestInterface.Module.class) - static class TrustSSLSockets { - @Provides SSLSocketFactory trustingSSLSocketFactory() { - return TrustingSSLSocketFactory.get(); - } - } - - @Test public void canOverrideSSLSocketFactory() throws IOException, InterruptedException { - MockWebServer server = new MockWebServer(); - server.useHttps(TrustingSSLSocketFactory.get("localhost"), false); - server.enqueue(new MockResponse().setBody("success!")); - server.play(); - - try { - TestInterface api = Feign.create(TestInterface.class, "https://localhost:" + server.getPort(), - new TrustSSLSockets()); - api.post(); - } finally { - server.shutdown(); - } - } - - @Module(overrides = true, includes = TrustSSLSockets.class) - static class DisableHostnameVerification { - @Provides HostnameVerifier acceptAllHostnameVerifier() { - return new HostnameVerifier() { - @Override - public boolean verify(String s, SSLSession sslSession) { - return true; - } - }; - } - } - - @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!")); - server.play(); - - try { - TestInterface api = Feign.create(TestInterface.class, "https://localhost:" + server.getPort(), - new DisableHostnameVerification()); - api.post(); - } finally { - server.shutdown(); - } - } - - @Test public void retriesFailedHandshake() throws IOException, InterruptedException { - MockWebServer server = new MockWebServer(); - server.useHttps(TrustingSSLSocketFactory.get("localhost"), false); - server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE)); - server.enqueue(new MockResponse().setBody("success!")); - server.play(); - - try { - TestInterface api = Feign.create(TestInterface.class, "https://localhost:" + server.getPort(), - new TestInterface.Module(), new TrustSSLSockets()); - api.post(); - assertEquals(2, server.getRequestCount()); - } finally { - server.shutdown(); - } - } - @Test public void equalsHashCodeAndToStringWork() { Target t1 = new HardCodedTarget(TestInterface.class, "http://localhost:8080"); Target t2 = new HardCodedTarget(TestInterface.class, "http://localhost:8888"); diff --git a/core/src/test/java/feign/client/DefaultClientTest.java b/core/src/test/java/feign/client/DefaultClientTest.java new file mode 100644 index 0000000000..066b944902 --- /dev/null +++ b/core/src/test/java/feign/client/DefaultClientTest.java @@ -0,0 +1,160 @@ +/* + * Copyright 2015 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.client; + +import com.squareup.okhttp.mockwebserver.MockResponse; +import com.squareup.okhttp.mockwebserver.SocketPolicy; +import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule; +import dagger.Lazy; +import feign.Client; +import feign.Feign; +import feign.FeignException; +import feign.Headers; +import feign.RequestLine; +import feign.Response; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.ProtocolException; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocketFactory; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static feign.Util.UTF_8; +import static feign.assertj.MockWebServerAssertions.assertThat; +import static java.util.Arrays.asList; +import static org.hamcrest.core.Is.isA; +import static org.junit.Assert.assertEquals; + +public class DefaultClientTest { + @Rule public final ExpectedException thrown = ExpectedException.none(); + @Rule public final MockWebServerRule server = new MockWebServerRule(); + + interface TestInterface { + @RequestLine("POST /?foo=bar&foo=baz&qux=") + @Headers({"Foo: Bar", "Foo: Baz", "Qux: ", "Content-Type: text/plain"}) Response post(String body); + + @RequestLine("PATCH /") String patch(); + } + + @Test public void parsesRequestAndResponse() throws IOException, InterruptedException { + server.enqueue(new MockResponse().setBody("foo").addHeader("Foo: Bar")); + + TestInterface api = Feign.builder().target(TestInterface.class, "http://localhost:" + server.getPort()); + + Response response = api.post("foo"); + + assertThat(response.status()).isEqualTo(200); + assertThat(response.reason()).isEqualTo("OK"); + assertThat(response.headers()) + .containsEntry("Content-Length", asList("3")) + .containsEntry("Foo", asList("Bar")); + assertThat(response.body().asInputStream()).hasContentEqualTo(new ByteArrayInputStream("foo".getBytes(UTF_8))); + + assertThat(server.takeRequest()).hasMethod("POST") + .hasPath("/?foo=bar&foo=baz&qux=") + .hasHeaders("Foo: Bar", "Foo: Baz", "Qux: ", "Content-Length: 3") + .hasBody("foo"); + } + + @Test public void parsesErrorResponse() throws IOException, InterruptedException { + thrown.expect(FeignException.class); + thrown.expectMessage("status 500 reading TestInterface#post(String); content:\n" + "ARGHH"); + + server.enqueue(new MockResponse().setResponseCode(500).setBody("ARGHH")); + + TestInterface api = Feign.builder().target(TestInterface.class, "http://localhost:" + server.getPort()); + + api.post("foo"); + } + + /** + * We currently don't include the 60-line workaround + * jersey uses to overcome the lack of support for PATCH. For now, prefer okhttp. + * + * @see java.net.HttpURLConnection#setRequestMethod + */ + @Test public void patchUnsupported() throws IOException, InterruptedException { + thrown.expectCause(isA(ProtocolException.class)); + + TestInterface api = Feign.builder().target(TestInterface.class, "http://localhost:" + server.getPort()); + + api.patch(); + } + + Client trustSSLSockets = new Client.Default(new Lazy() { + @Override public SSLSocketFactory get() { + return TrustingSSLSocketFactory.get(); + } + }, new Lazy() { + @Override public HostnameVerifier get() { + return HttpsURLConnection.getDefaultHostnameVerifier(); + } + }); + + @Test public void canOverrideSSLSocketFactory() throws IOException, InterruptedException { + server.get().useHttps(TrustingSSLSocketFactory.get("localhost"), false); + server.enqueue(new MockResponse()); + + TestInterface api = Feign.builder() + .client(trustSSLSockets) + .target(TestInterface.class, "https://localhost:" + server.getPort()); + + api.post("foo"); + } + + Client disableHostnameVerification = new Client.Default(new Lazy() { + @Override public SSLSocketFactory get() { + return TrustingSSLSocketFactory.get(); + } + }, new Lazy() { + @Override public HostnameVerifier get() { + return new HostnameVerifier() { + @Override + public boolean verify(String s, SSLSession sslSession) { + return true; + } + }; + } + }); + + @Test public void canOverrideHostnameVerifier() throws IOException, InterruptedException { + server.get().useHttps(TrustingSSLSocketFactory.get("bad.example.com"), false); + server.enqueue(new MockResponse()); + + TestInterface api = Feign.builder() + .client(disableHostnameVerification) + .target(TestInterface.class, "https://localhost:" + server.getPort()); + + api.post("foo"); + } + + @Test public void retriesFailedHandshake() throws IOException, InterruptedException { + server.get().useHttps(TrustingSSLSocketFactory.get("localhost"), false); + server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE)); + server.enqueue(new MockResponse()); + + TestInterface api = Feign.builder() + .client(trustSSLSockets) + .target(TestInterface.class, "https://localhost:" + server.getPort()); + + api.post("foo"); + assertEquals(2, server.getRequestCount()); + } +} diff --git a/core/src/test/java/feign/TrustingSSLSocketFactory.java b/core/src/test/java/feign/client/TrustingSSLSocketFactory.java similarity index 99% rename from core/src/test/java/feign/TrustingSSLSocketFactory.java rename to core/src/test/java/feign/client/TrustingSSLSocketFactory.java index 98723a1cbb..adbddcb639 100644 --- a/core/src/test/java/feign/TrustingSSLSocketFactory.java +++ b/core/src/test/java/feign/client/TrustingSSLSocketFactory.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package feign; +package feign.client; import java.io.IOException; import java.io.InputStream; From 49b700e6f7850885facea99d426d01f3911f4c29 Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Mon, 26 Jan 2015 01:15:41 -0800 Subject: [PATCH 158/179] Adds OkHttp integration closes #134 --- CHANGELOG.md | 1 + README.md | 11 ++ core/src/main/java/feign/Response.java | 4 + .../java/feign/codec/DefaultDecoderTest.java | 2 +- .../feign/codec/DefaultErrorDecoderTest.java | 4 +- .../test/java/feign/gson/GsonModuleTest.java | 2 +- .../java/feign/jackson/JacksonModuleTest.java | 2 +- okhttp/README.md | 12 ++ okhttp/build.gradle | 12 ++ .../main/java/feign/okhttp/OkHttpClient.java | 131 ++++++++++++++++++ .../java/feign/okhttp/OkHttpClientTest.java | 103 ++++++++++++++ .../test/java/feign/sax/SAXDecoderTest.java | 2 +- settings.gradle | 2 +- 13 files changed, 281 insertions(+), 7 deletions(-) create mode 100644 okhttp/README.md create mode 100644 okhttp/build.gradle create mode 100644 okhttp/src/main/java/feign/okhttp/OkHttpClient.java create mode 100644 okhttp/src/test/java/feign/okhttp/OkHttpClientTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index f898bd8d63..904b4b7039 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ### Version 7.1 * Introduces feign.@Param to annotate template parameters. Users must migrate from `javax.inject.@Named` to `feign.@Param` before updating to Feign 8.0. +* Adds OkHttp integration * Allows multiple headers with the same name. ### Version 7.0 diff --git a/README.md b/README.md index 2aec15053e..297495d863 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,17 @@ GitHub github = Feign.builder() .contract(new JAXRSModule.JAXRSContract()) .target(GitHub.class, "https://api.github.com"); ``` +### OkHttp +[OkHttpClient](https://github.com/Netflix/feign/tree/master/okhttp) directs Feign's http requests to [OkHttp](http://square.github.io/okhttp/), which enables SPDY and better network control. + +To use OkHttp with Feign, add the OkHttp module to your classpath. Then, configure Feign to use the OkHttpClient: + +```java +GitHub github = Feign.builder() + .client(new OkHttpClient()) + .target(GitHub.class, "https://api.github.com"); +``` + ### Ribbon [RibbonModule](https://github.com/Netflix/feign/tree/master/ribbon) overrides URL resolution of Feign's client, adding smart routing and resiliency capabilities provided by [Ribbon](https://github.com/Netflix/ribbon). diff --git a/core/src/main/java/feign/Response.java b/core/src/main/java/feign/Response.java index 2324254d74..4ea1941d13 100644 --- a/core/src/main/java/feign/Response.java +++ b/core/src/main/java/feign/Response.java @@ -58,6 +58,10 @@ public static Response create(int status, String reason, Map> headers, Body body) { + return new Response(status, reason, headers, body); + } + private Response(int status, String reason, Map> headers, Body body) { checkState(status >= 200, "Invalid status code: %s", status); this.status = status; diff --git a/core/src/test/java/feign/codec/DefaultDecoderTest.java b/core/src/test/java/feign/codec/DefaultDecoderTest.java index c15057706d..02c86c167f 100644 --- a/core/src/test/java/feign/codec/DefaultDecoderTest.java +++ b/core/src/test/java/feign/codec/DefaultDecoderTest.java @@ -70,6 +70,6 @@ private Response knownResponse() { } private Response nullBodyResponse() { - return Response.create(200, "OK", Collections.>emptyMap(), null); + return Response.create(200, "OK", Collections.>emptyMap(), (byte[]) null); } } diff --git a/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java b/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java index d78b022e12..1fd443feee 100644 --- a/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java +++ b/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java @@ -39,7 +39,7 @@ public class DefaultErrorDecoderTest { thrown.expect(FeignException.class); thrown.expectMessage("status 500 reading Service#foo()"); - Response response = Response.create(500, "Internal server error", headers, null); + Response response = Response.create(500, "Internal server error", headers, (byte[]) null); throw errorDecoder.decode("Service#foo()", response); } @@ -58,7 +58,7 @@ public class DefaultErrorDecoderTest { thrown.expectMessage("status 503 reading Service#foo()"); headers.put(RETRY_AFTER, Arrays.asList("Sat, 1 Jan 2000 00:00:00 GMT")); - Response response = Response.create(503, "Service Unavailable", headers, null); + Response response = Response.create(503, "Service Unavailable", headers, (byte[]) null); throw errorDecoder.decode("Service#foo()", response); } diff --git a/gson/src/test/java/feign/gson/GsonModuleTest.java b/gson/src/test/java/feign/gson/GsonModuleTest.java index fdf95cf0cb..bf6e1ada25 100644 --- a/gson/src/test/java/feign/gson/GsonModuleTest.java +++ b/gson/src/test/java/feign/gson/GsonModuleTest.java @@ -141,7 +141,7 @@ static class DecoderBindings { DecoderBindings bindings = new DecoderBindings(); ObjectGraph.create(bindings).inject(bindings); - Response response = Response.create(204, "OK", Collections.>emptyMap(), null); + Response response = Response.create(204, "OK", Collections.>emptyMap(), (byte[]) null); assertNull(bindings.decoder.decode(response, String.class)); } diff --git a/jackson/src/test/java/feign/jackson/JacksonModuleTest.java b/jackson/src/test/java/feign/jackson/JacksonModuleTest.java index 2aa8f9f0a7..698feb324d 100644 --- a/jackson/src/test/java/feign/jackson/JacksonModuleTest.java +++ b/jackson/src/test/java/feign/jackson/JacksonModuleTest.java @@ -128,7 +128,7 @@ static class DecoderBindings { DecoderBindings bindings = new DecoderBindings(); ObjectGraph.create(bindings).inject(bindings); - Response response = Response.create(204, "OK", Collections.>emptyMap(), null); + Response response = Response.create(204, "OK", Collections.>emptyMap(), (byte[]) null); assertNull(bindings.decoder.decode(response, String.class)); } diff --git a/okhttp/README.md b/okhttp/README.md new file mode 100644 index 0000000000..81f68373eb --- /dev/null +++ b/okhttp/README.md @@ -0,0 +1,12 @@ +OkHttp +=================== + +This module directs Feign's http requests to [OkHttp](http://square.github.io/okhttp/), which enables SPDY and better network control. + +To use OkHttp with Feign, add the OkHttp module to your classpath. Then, configure Feign to use the OkHttpClient: + +```java +GitHub github = Feign.builder() + .client(new OkHttpClient()) + .target(GitHub.class, "https://api.github.com"); +``` diff --git a/okhttp/build.gradle b/okhttp/build.gradle new file mode 100644 index 0000000000..a01cbef386 --- /dev/null +++ b/okhttp/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'java' + +sourceCompatibility = 1.6 + +dependencies { + compile project(':feign-core') + compile 'com.squareup.okhttp:okhttp:2.2.0' + testCompile 'junit:junit:4.12' + testCompile 'org.assertj:assertj-core:1.7.1' + testCompile 'com.squareup.okhttp:mockwebserver:2.2.0' + testCompile project(':feign-core').sourceSets.test.output // for assertions +} diff --git a/okhttp/src/main/java/feign/okhttp/OkHttpClient.java b/okhttp/src/main/java/feign/okhttp/OkHttpClient.java new file mode 100644 index 0000000000..35e74af6ea --- /dev/null +++ b/okhttp/src/main/java/feign/okhttp/OkHttpClient.java @@ -0,0 +1,131 @@ +/* + * Copyright 2015 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.okhttp; + +import com.squareup.okhttp.Headers; +import com.squareup.okhttp.MediaType; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.RequestBody; +import com.squareup.okhttp.Response; +import com.squareup.okhttp.ResponseBody; +import feign.Client; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * This module directs Feign's http requests to OkHttp, which enables + * SPDY and better network control. + * Ex. + *
+ * GitHub github = Feign.builder().client(new OkHttpClient()).target(GitHub.class, "https://api.github.com");
+ */
+public final class OkHttpClient implements Client {
+  private final com.squareup.okhttp.OkHttpClient delegate;
+
+  public OkHttpClient() {
+    this(new com.squareup.okhttp.OkHttpClient());
+  }
+
+  public OkHttpClient(com.squareup.okhttp.OkHttpClient delegate) {
+    this.delegate = delegate;
+  }
+
+  @Override public feign.Response execute(feign.Request input, feign.Request.Options options) throws IOException {
+    com.squareup.okhttp.OkHttpClient requestScoped;
+    if (delegate.getConnectTimeout() != options.connectTimeoutMillis()
+        || delegate.getReadTimeout() != options.readTimeoutMillis()) {
+      requestScoped = delegate.clone();
+      requestScoped.setConnectTimeout(options.connectTimeoutMillis(), TimeUnit.MILLISECONDS);
+      requestScoped.setReadTimeout(options.readTimeoutMillis(), TimeUnit.MILLISECONDS);
+    } else {
+      requestScoped = delegate;
+    }
+    Request request = toOkHttpRequest(input);
+    Response response = requestScoped.newCall(request).execute();
+    return toFeignResponse(response);
+  }
+
+  static Request toOkHttpRequest(feign.Request input) {
+    Request.Builder requestBuilder = new Request.Builder();
+    requestBuilder.url(input.url());
+
+    MediaType mediaType = null;
+    for (String field : input.headers().keySet()) {
+      for (String value : input.headers().get(field)) {
+        if (field.equalsIgnoreCase("Content-Type")) {
+          mediaType = MediaType.parse(value);
+          if (input.charset() != null) mediaType.charset(input.charset());
+        } else {
+          requestBuilder.addHeader(field, value);
+        }
+      }
+    }
+    RequestBody body = input.body() != null ? RequestBody.create(mediaType, input.body()) : null;
+    requestBuilder.method(input.method(), body);
+    return requestBuilder.build();
+  }
+
+  private static feign.Response toFeignResponse(Response input) {
+    return feign.Response.create(input.code(), input.message(), toMap(input.headers()), toBody(input.body()));
+  }
+
+  private static Map> toMap(Headers headers) {
+    Map> result = new LinkedHashMap>(headers.size());
+    for (String name : headers.names()) {
+      // TODO: this is very inefficient as headers.values iterate case insensitively.
+      result.put(name, headers.values(name));
+    }
+    return result;
+  }
+
+  private static feign.Response.Body toBody(final ResponseBody input) {
+    if (input == null || input.contentLength() == 0) {
+      return null;
+    }
+    if (input.contentLength() > Integer.MAX_VALUE) {
+      throw new UnsupportedOperationException("Length too long "+ input.contentLength());
+    }
+    final Integer length = input.contentLength() != -1 ? (int) input.contentLength() : null;
+
+    return new feign.Response.Body() {
+
+      @Override public void close() throws IOException {
+        input.close();
+      }
+
+      @Override public Integer length() {
+        return length;
+      }
+
+      @Override public boolean isRepeatable() {
+        return false;
+      }
+
+      @Override public InputStream asInputStream() throws IOException {
+        return input.byteStream();
+      }
+
+      @Override public Reader asReader() throws IOException {
+        return input.charStream();
+      }
+    };
+  }
+}
diff --git a/okhttp/src/test/java/feign/okhttp/OkHttpClientTest.java b/okhttp/src/test/java/feign/okhttp/OkHttpClientTest.java
new file mode 100644
index 0000000000..c17e300d10
--- /dev/null
+++ b/okhttp/src/test/java/feign/okhttp/OkHttpClientTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2015 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.okhttp;
+
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.SocketPolicy;
+import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
+import dagger.Lazy;
+import feign.Client;
+import feign.Feign;
+import feign.FeignException;
+import feign.Headers;
+import feign.RequestLine;
+import feign.Response;
+import feign.Util;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static feign.Util.UTF_8;
+import static feign.assertj.MockWebServerAssertions.assertThat;
+import static java.util.Arrays.asList;
+import static org.assertj.core.data.MapEntry.entry;
+import static org.hamcrest.core.Is.isA;
+import static org.junit.Assert.assertEquals;
+
+public class OkHttpClientTest {
+  @Rule public final ExpectedException thrown = ExpectedException.none();
+  @Rule public final MockWebServerRule server = new MockWebServerRule();
+
+  interface TestInterface {
+    @RequestLine("POST /?foo=bar&foo=baz&qux=")
+    @Headers({"Foo: Bar", "Foo: Baz", "Qux: ", "Content-Type: text/plain"}) Response post(String body);
+
+    @RequestLine("PATCH /") String patch();
+  }
+
+  @Test public void parsesRequestAndResponse() throws IOException, InterruptedException {
+    server.enqueue(new MockResponse().setBody("foo").addHeader("Foo: Bar"));
+
+    TestInterface api = Feign.builder()
+        .client(new OkHttpClient())
+        .target(TestInterface.class, "http://localhost:" + server.getPort());
+
+    Response response = api.post("foo");
+
+    assertThat(response.status()).isEqualTo(200);
+    assertThat(response.reason()).isEqualTo("OK");
+    assertThat(response.headers())
+        .containsEntry("Content-Length", asList("3"))
+        .containsEntry("Foo", asList("Bar"));
+    assertThat(response.body().asInputStream()).hasContentEqualTo(new ByteArrayInputStream("foo".getBytes(UTF_8)));
+
+    assertThat(server.takeRequest()).hasMethod("POST")
+        .hasPath("/?foo=bar&foo=baz&qux=")
+        .hasHeaders("Foo: Bar", "Foo: Baz", "Qux: ", "Content-Length: 3")
+        .hasBody("foo");
+  }
+
+  @Test public void parsesErrorResponse() throws IOException, InterruptedException {
+    thrown.expect(FeignException.class);
+    thrown.expectMessage("status 500 reading TestInterface#post(String); content:\n" + "ARGHH");
+
+    server.enqueue(new MockResponse().setResponseCode(500).setBody("ARGHH"));
+
+    TestInterface api = Feign.builder()
+        .client(new OkHttpClient())
+        .target(TestInterface.class, "http://localhost:" + server.getPort());
+
+    api.post("foo");
+  }
+
+  @Test public void patch() throws IOException, InterruptedException {
+    server.enqueue(new MockResponse().setBody("foo"));
+    server.enqueue(new MockResponse());
+
+    TestInterface api = Feign.builder()
+        .client(new OkHttpClient())
+        .target(TestInterface.class, "http://localhost:" + server.getPort());
+
+    assertEquals("foo", api.patch());
+
+    assertThat(server.takeRequest())
+        .hasHeaders("Content-Length: 0") // Note: OkHttp adds content length.
+        .hasNoHeaderNamed("Content-Type")
+        .hasMethod("PATCH");
+  }
+}
diff --git a/sax/src/test/java/feign/sax/SAXDecoderTest.java b/sax/src/test/java/feign/sax/SAXDecoderTest.java
index cd3de0ec6d..f627464519 100644
--- a/sax/src/test/java/feign/sax/SAXDecoderTest.java
+++ b/sax/src/test/java/feign/sax/SAXDecoderTest.java
@@ -142,7 +142,7 @@ public void characters(char ch[], int start, int length) {
   }
 
   @Test public void nullBodyDecodesToNull() throws Exception {
-    Response response = Response.create(204, "OK", Collections.>emptyMap(), null);
+    Response response = Response.create(204, "OK", Collections.>emptyMap(), (byte[]) null);
     assertNull(decoder.decode(response, String.class));
   }
 }
diff --git a/settings.gradle b/settings.gradle
index 6f6dc626f5..3b5fdad827 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,5 +1,5 @@
 rootProject.name='feign'
-include 'core', 'sax', 'gson', 'jackson', 'jaxb', 'jaxrs', 'ribbon', 'slf4j', 'example-github', 'example-wikipedia'
+include 'core', 'sax', 'gson', 'jackson', 'jaxb', 'jaxrs', 'okhttp', 'ribbon', 'slf4j', 'example-github', 'example-wikipedia'
 
 rootProject.children.each { childProject ->
     childProject.name = 'feign-' + childProject.name

From 72c3ba80a6cd132bc2067110b02e0b208eff8763 Mon Sep 17 00:00:00 2001
From: Adrian Cole 
Date: Mon, 26 Jan 2015 01:42:18 -0800
Subject: [PATCH 159/179] Ensures Accept headers default to */*

Closes #123
---
 CHANGELOG.md                                            | 1 +
 core/src/main/java/feign/Client.java                    | 4 ++++
 core/src/test/java/feign/client/DefaultClientTest.java  | 4 ++--
 okhttp/src/main/java/feign/okhttp/OkHttpClient.java     | 6 ++++++
 okhttp/src/test/java/feign/okhttp/OkHttpClientTest.java | 6 +++---
 5 files changed, 16 insertions(+), 5 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 904b4b7039..4139130823 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
 * Introduces feign.@Param to annotate template parameters. Users must migrate from `javax.inject.@Named` to `feign.@Param` before updating to Feign 8.0.
 * Adds OkHttp integration
 * Allows multiple headers with the same name.
+* Ensures Accept headers default to `*/*`
 
 ### Version 7.0
 * Expose reflective dispatch hook: InvocationHandlerFactory
diff --git a/core/src/main/java/feign/Client.java b/core/src/main/java/feign/Client.java
index aab143daa1..6ba3796f43 100644
--- a/core/src/main/java/feign/Client.java
+++ b/core/src/main/java/feign/Client.java
@@ -84,8 +84,10 @@ HttpURLConnection convertAndSend(Request request, Options options) throws IOExce
       Collection contentEncodingValues = request.headers().get(CONTENT_ENCODING);
       boolean gzipEncodedRequest = contentEncodingValues != null && contentEncodingValues.contains(ENCODING_GZIP);
 
+      boolean hasAcceptHeader = false;
       Integer contentLength = null;
       for (String field : request.headers().keySet()) {
+        if (field.equalsIgnoreCase("Accept")) hasAcceptHeader = true;
         for (String value : request.headers().get(field)) {
           if (field.equals(CONTENT_LENGTH)) {
             if (!gzipEncodedRequest) {
@@ -97,6 +99,8 @@ HttpURLConnection convertAndSend(Request request, Options options) throws IOExce
           }
         }
       }
+      // Some servers choke on the default accept string.
+      if (!hasAcceptHeader) connection.addRequestProperty("Accept", "*/*");
 
       if (request.body() != null) {
         if (contentLength != null) {
diff --git a/core/src/test/java/feign/client/DefaultClientTest.java b/core/src/test/java/feign/client/DefaultClientTest.java
index 066b944902..c100375c7f 100644
--- a/core/src/test/java/feign/client/DefaultClientTest.java
+++ b/core/src/test/java/feign/client/DefaultClientTest.java
@@ -50,7 +50,7 @@ interface TestInterface {
     @RequestLine("POST /?foo=bar&foo=baz&qux=")
     @Headers({"Foo: Bar", "Foo: Baz", "Qux: ", "Content-Type: text/plain"}) Response post(String body);
 
-    @RequestLine("PATCH /") String patch();
+    @RequestLine("PATCH /") @Headers("Accept: text/plain") String patch();
   }
 
   @Test public void parsesRequestAndResponse() throws IOException, InterruptedException {
@@ -69,7 +69,7 @@ interface TestInterface {
 
     assertThat(server.takeRequest()).hasMethod("POST")
         .hasPath("/?foo=bar&foo=baz&qux=")
-        .hasHeaders("Foo: Bar", "Foo: Baz", "Qux: ", "Content-Length: 3")
+        .hasHeaders("Foo: Bar", "Foo: Baz", "Qux: ", "Accept: */*", "Content-Length: 3")
         .hasBody("foo");
   }
 
diff --git a/okhttp/src/main/java/feign/okhttp/OkHttpClient.java b/okhttp/src/main/java/feign/okhttp/OkHttpClient.java
index 35e74af6ea..042e6c7846 100644
--- a/okhttp/src/main/java/feign/okhttp/OkHttpClient.java
+++ b/okhttp/src/main/java/feign/okhttp/OkHttpClient.java
@@ -68,7 +68,10 @@ static Request toOkHttpRequest(feign.Request input) {
     requestBuilder.url(input.url());
 
     MediaType mediaType = null;
+    boolean hasAcceptHeader = false;
     for (String field : input.headers().keySet()) {
+      if (field.equalsIgnoreCase("Accept")) hasAcceptHeader = true;
+
       for (String value : input.headers().get(field)) {
         if (field.equalsIgnoreCase("Content-Type")) {
           mediaType = MediaType.parse(value);
@@ -78,6 +81,9 @@ static Request toOkHttpRequest(feign.Request input) {
         }
       }
     }
+    // Some servers choke on the default accept string.
+    if (!hasAcceptHeader) requestBuilder.addHeader("Accept", "*/*");
+
     RequestBody body = input.body() != null ? RequestBody.create(mediaType, input.body()) : null;
     requestBuilder.method(input.method(), body);
     return requestBuilder.build();
diff --git a/okhttp/src/test/java/feign/okhttp/OkHttpClientTest.java b/okhttp/src/test/java/feign/okhttp/OkHttpClientTest.java
index c17e300d10..881c9ef2e1 100644
--- a/okhttp/src/test/java/feign/okhttp/OkHttpClientTest.java
+++ b/okhttp/src/test/java/feign/okhttp/OkHttpClientTest.java
@@ -47,7 +47,7 @@ interface TestInterface {
     @RequestLine("POST /?foo=bar&foo=baz&qux=")
     @Headers({"Foo: Bar", "Foo: Baz", "Qux: ", "Content-Type: text/plain"}) Response post(String body);
 
-    @RequestLine("PATCH /") String patch();
+    @RequestLine("PATCH /") @Headers("Accept: text/plain") String patch();
   }
 
   @Test public void parsesRequestAndResponse() throws IOException, InterruptedException {
@@ -68,7 +68,7 @@ interface TestInterface {
 
     assertThat(server.takeRequest()).hasMethod("POST")
         .hasPath("/?foo=bar&foo=baz&qux=")
-        .hasHeaders("Foo: Bar", "Foo: Baz", "Qux: ", "Content-Length: 3")
+        .hasHeaders("Foo: Bar", "Foo: Baz", "Qux: ", "Accept: */*", "Content-Length: 3")
         .hasBody("foo");
   }
 
@@ -96,7 +96,7 @@ interface TestInterface {
     assertEquals("foo", api.patch());
 
     assertThat(server.takeRequest())
-        .hasHeaders("Content-Length: 0") // Note: OkHttp adds content length.
+        .hasHeaders("Accept: text/plain", "Content-Length: 0") // Note: OkHttp adds content length.
         .hasNoHeaderNamed("Content-Type")
         .hasMethod("PATCH");
   }

From 944e20e0e7daa71d4a4179ec5f617b94602b0844 Mon Sep 17 00:00:00 2001
From: Rob Spieldenner 
Date: Mon, 26 Jan 2015 09:40:12 -0800
Subject: [PATCH 160/179] Move to nebula.netflixoss 2.2.5

---
 build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/build.gradle b/build.gradle
index 1c48a3fb11..d8579d81d2 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,5 +1,5 @@
 plugins {
-    id 'nebula.netflixoss' version '2.2.2'
+    id 'nebula.netflixoss' version '2.2.5'
 }
 
 ext {

From 3bddf1f0aa3098812e9cfa232d2ce184e4d05d60 Mon Sep 17 00:00:00 2001
From: Adrian Cole 
Date: Mon, 26 Jan 2015 09:36:50 -0800
Subject: [PATCH 161/179] Supports custom expansion of template parameters via
 Param.Expander

Parameters annotated with `Param` expand based on their `toString`. By
specifying a custom `Param.Expander`, users can control this behavior,
for example formatting dates.

```java
@RequestLine("GET /?since={date}") Result list(@Param(value = "date", expander = DateToMillis.class) Date date);
```

Closes #122
---
 CHANGELOG.md                                  |  1 +
 README.md                                     | 10 +++++++++
 core/src/main/java/feign/Contract.java        | 10 +++++++--
 core/src/main/java/feign/MethodMetadata.java  | 11 ++++++----
 core/src/main/java/feign/Param.java           | 17 ++++++++++++++-
 core/src/main/java/feign/ReflectiveFeign.java | 16 ++++++++++++++
 core/src/main/java/feign/RequestTemplate.java |  2 +-
 core/src/main/java/feign/codec/Encoder.java   |  2 +-
 .../test/java/feign/DefaultContractTest.java  | 18 ++++++++++++++++
 core/src/test/java/feign/FeignTest.java       | 21 +++++++++++++++++++
 10 files changed, 99 insertions(+), 9 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4139130823..7f11e71227 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
 ### Version 7.1
 * Introduces feign.@Param to annotate template parameters. Users must migrate from `javax.inject.@Named` to `feign.@Param` before updating to Feign 8.0.
+  * Supports custom expansion via `@Param(value = "name", expander = CustomExpander.class)`
 * Adds OkHttp integration
 * Allows multiple headers with the same name.
 * Ensures Accept headers default to `*/*`
diff --git a/README.md b/README.md
index 297495d863..1d91b4c7f8 100644
--- a/README.md
+++ b/README.md
@@ -228,6 +228,16 @@ Where possible, Feign configuration uses normal Dagger conventions.  For example
   };
 }
 ```
+
+#### Custom Parameter Expansion
+Parameters annotated with `Param` expand based on their `toString`. By
+specifying a custom `Param.Expander`, users can control this behavior,
+for example formatting dates.
+
+```java
+@RequestLine("GET /?since={date}") Result list(@Param(value = "date", expander = DateToMillis.class) Date date);
+```
+
 #### Logging
 You can log the http messages going to and from the target by setting up a `Logger`.  Here's the easiest way to do that:
 ```java
diff --git a/core/src/main/java/feign/Contract.java b/core/src/main/java/feign/Contract.java
index 2001fa9427..6be6e49ec1 100644
--- a/core/src/main/java/feign/Contract.java
+++ b/core/src/main/java/feign/Contract.java
@@ -38,7 +38,7 @@ public interface Contract {
    */
   List parseAndValidatateMetadata(Class declaring);
 
-  public static abstract class BaseContract implements Contract {
+  abstract class BaseContract implements Contract {
 
     @Override public List parseAndValidatateMetadata(Class declaring) {
       List metadata = new ArrayList();
@@ -119,7 +119,7 @@ protected void nameParam(MethodMetadata data, String name, int i) {
     }
   }
 
-  static class Default extends BaseContract {
+  class Default extends BaseContract {
 
     @Override
     protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) {
@@ -173,6 +173,12 @@ protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[
           checkState(emptyToNull(name) != null,
               "%s annotation was empty on param %s.", annotationType.getSimpleName(), paramIndex);
           nameParam(data, name, paramIndex);
+          if (annotationType == Param.class) {
+            Class expander = ((Param) annotation).expander();
+            if (expander != Param.ToStringExpander.class) {
+              data.indexToExpanderClass().put(paramIndex, expander);
+            }
+          }
           isHttpAnnotation = true;
           String varName = '{' + name + '}';
           if (data.template().url().indexOf(varName) == -1 &&
diff --git a/core/src/main/java/feign/MethodMetadata.java b/core/src/main/java/feign/MethodMetadata.java
index d2c8f3a5d2..bca3678cac 100644
--- a/core/src/main/java/feign/MethodMetadata.java
+++ b/core/src/main/java/feign/MethodMetadata.java
@@ -15,6 +15,7 @@
  */
 package feign;
 
+import feign.Param.Expander;
 import java.io.Serializable;
 import java.lang.reflect.Type;
 import java.util.ArrayList;
@@ -36,6 +37,8 @@ public final class MethodMetadata implements Serializable {
   private RequestTemplate template = new RequestTemplate();
   private List formParams = new ArrayList();
   private Map> indexToName = new LinkedHashMap>();
+  private Map> indexToExpanderClass =
+      new LinkedHashMap>();
 
   /**
    * @see Feign#configKey(java.lang.reflect.Method)
@@ -49,9 +52,6 @@ MethodMetadata configKey(String configKey) {
     return this;
   }
 
-  /**
-   * Method return type.
-   */
   public Type returnType() {
     return returnType;
   }
@@ -100,6 +100,9 @@ public Map> indexToName() {
     return indexToName;
   }
 
-  private static final long serialVersionUID = 1L;
+  public Map> indexToExpanderClass() {
+    return indexToExpanderClass;
+  }
 
+  private static final long serialVersionUID = 1L;
 }
diff --git a/core/src/main/java/feign/Param.java b/core/src/main/java/feign/Param.java
index d62e4decba..e26964feee 100644
--- a/core/src/main/java/feign/Param.java
+++ b/core/src/main/java/feign/Param.java
@@ -20,9 +20,24 @@
 import static java.lang.annotation.ElementType.PARAMETER;
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-/** The name of a template variable applied to {@link Headers},  {@linkplain RequestLine} or {@linkplain Body} */
+/** A named template parameter applied to {@link Headers}, {@linkplain RequestLine} or {@linkplain Body} */
 @Retention(RUNTIME)
 @java.lang.annotation.Target(PARAMETER)
 public @interface Param {
+  /** The name of the template parameter. */
   String value();
+
+  /** How to expand the value of this parameter, if {@link ToStringExpander} isn't adequate. */
+  Class expander() default ToStringExpander.class;
+
+  interface Expander {
+    /** Expands the value into a string. Does not accept or return null. */
+    String expand(Object value);
+  }
+
+  final class ToStringExpander implements Expander {
+    @Override public String expand(Object value) {
+      return value.toString();
+    }
+  }
 }
diff --git a/core/src/main/java/feign/ReflectiveFeign.java b/core/src/main/java/feign/ReflectiveFeign.java
index 6a39d6d8bb..4b19c84732 100644
--- a/core/src/main/java/feign/ReflectiveFeign.java
+++ b/core/src/main/java/feign/ReflectiveFeign.java
@@ -17,6 +17,7 @@
 
 import dagger.Provides;
 import feign.InvocationHandlerFactory.MethodHandler;
+import feign.Param.Expander;
 import feign.Request.Options;
 import feign.codec.Decoder;
 import feign.codec.EncodeException;
@@ -158,9 +159,20 @@ public Map apply(Target key) {
 
   private static class BuildTemplateByResolvingArgs implements RequestTemplate.Factory {
     protected final MethodMetadata metadata;
+    private final Map indexToExpander = new LinkedHashMap();
 
     private BuildTemplateByResolvingArgs(MethodMetadata metadata) {
       this.metadata = metadata;
+      if (metadata.indexToExpanderClass().isEmpty()) return;
+      for (Entry> indexToExpanderClass : metadata.indexToExpanderClass().entrySet()) {
+        try {
+          indexToExpander.put(indexToExpanderClass.getKey(), indexToExpanderClass.getValue().newInstance());
+        } catch (InstantiationException e) {
+          throw new IllegalStateException(e);
+        } catch (IllegalAccessException e) {
+          throw new IllegalStateException(e);
+        }
+      }
     }
 
     @Override public RequestTemplate create(Object[] argv) {
@@ -172,8 +184,12 @@ private BuildTemplateByResolvingArgs(MethodMetadata metadata) {
       }
       Map varBuilder = new LinkedHashMap();
       for (Entry> entry : metadata.indexToName().entrySet()) {
+        int i = entry.getKey();
         Object value = argv[entry.getKey()];
         if (value != null) { // Null values are skipped.
+          if (indexToExpander.containsKey(i)) {
+            value = indexToExpander.get(i).expand(value);
+          }
           for (String name : entry.getValue())
             varBuilder.put(name, value);
         }
diff --git a/core/src/main/java/feign/RequestTemplate.java b/core/src/main/java/feign/RequestTemplate.java
index 8dc652cbe4..081369d569 100644
--- a/core/src/main/java/feign/RequestTemplate.java
+++ b/core/src/main/java/feign/RequestTemplate.java
@@ -80,7 +80,7 @@ public RequestTemplate(RequestTemplate toCopy) {
   }
 
   /**
-   * Resolves any templated variables in the requests path, query, or headers
+   * Resolves any template parameters in the requests path, query, or headers
    * against the supplied unencoded arguments.
    * 
*

relationship to JAXRS 2.0
diff --git a/core/src/main/java/feign/codec/Encoder.java b/core/src/main/java/feign/codec/Encoder.java index b743d3423f..f9ba93fe8b 100644 --- a/core/src/main/java/feign/codec/Encoder.java +++ b/core/src/main/java/feign/codec/Encoder.java @@ -71,7 +71,7 @@ public interface Encoder { /** * Default implementation of {@code Encoder}. */ - public class Default implements Encoder { + class Default implements Encoder { @Override public void encode(Object object, RequestTemplate template) throws EncodeException { if (object instanceof String) { diff --git a/core/src/test/java/feign/DefaultContractTest.java b/core/src/test/java/feign/DefaultContractTest.java index 495c7f03fa..1906e75bea 100644 --- a/core/src/test/java/feign/DefaultContractTest.java +++ b/core/src/test/java/feign/DefaultContractTest.java @@ -17,6 +17,7 @@ import com.google.gson.reflect.TypeToken; import java.net.URI; +import java.util.Date; import java.util.List; import javax.inject.Named; import org.junit.Rule; @@ -247,6 +248,23 @@ interface HeaderParams { .containsExactly(entry(0, asList("Auth-Token"))); } + interface CustomExpander { + @RequestLine("POST /?date={date}") void date(@Param(value = "date", expander = DateToMillis.class) Date date); + } + + class DateToMillis implements Param.Expander { + @Override public String expand(Object value) { + return String.valueOf(((Date) value).getTime()); + } + } + + @Test public void customExpander() throws Exception { + MethodMetadata md = contract.parseAndValidatateMetadata(CustomExpander.class.getDeclaredMethod("date", Date.class)); + + assertThat(md.indexToExpanderClass()) + .containsExactly(entry(0, DateToMillis.class)); + } + // TODO: remove all of below in 8.x interface WithPathAndQueryParamsAnnotatedWithNamed { diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index 9f29831ed7..fbf0f20e40 100644 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -30,6 +30,7 @@ import java.lang.reflect.Type; import java.net.URI; import java.util.Arrays; +import java.util.Date; import java.util.List; import java.util.Map; import javax.inject.Singleton; @@ -70,6 +71,14 @@ void login( @RequestLine("GET /?1={1}&2={2}") Response queryParams(@Param("1") String one, @Param("2") Iterable twos); + @RequestLine("POST /?date={date}") void expand(@Param(value = "date", expander = DateToMillis.class) Date date); + + class DateToMillis implements Param.Expander { + @Override public String expand(Object value) { + return String.valueOf(((Date) value).getTime()); + } + } + @dagger.Module(injects = Feign.class, addsTo = Feign.Defaults.class) static class Module { @Provides Decoder defaultDecoder() { @@ -224,6 +233,18 @@ public void multipleInterceptor() throws IOException, InterruptedException { .hasHeaders("X-Forwarded-For: origin.host.com", "User-Agent: Feign"); } + @Test public void customExpander() throws Exception { + server.enqueue(new MockResponse()); + + TestInterface api = + Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module()); + + api.expand(new Date(1234l)); + + assertThat(server.takeRequest()) + .hasPath("/?date=1234"); + } + @Test public void toKeyMethodFormatsAsExpected() throws Exception { assertEquals("TestInterface#post()", Feign.configKey(TestInterface.class.getDeclaredMethod("post"))); assertEquals("TestInterface#uriParam(String,URI,String)", From 8a0cba5cac66638905ff1ceff36c70997c373782 Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Sat, 24 Jan 2015 17:04:48 +0800 Subject: [PATCH 162/179] Removes Dagger 1.x Dependency and support for javax.inject.Named Dagger 1.x and 2.x are incompatible. Rather than choose one over the other, this change removes Dagger completely. Users can now choose any injector, constructing Feign via its Builder. This change also drops support for javax.inject.Named, which has been replaced by feign.Param. see #120 --- CHANGELOG.md | 4 + README.md | 104 ++++---- build.gradle | 1 - core/build.gradle | 3 +- core/src/main/java/feign/Client.java | 17 +- core/src/main/java/feign/Contract.java | 10 +- core/src/main/java/feign/Feign.java | 171 ++----------- .../java/feign/InvocationHandlerFactory.java | 1 + core/src/main/java/feign/ReflectiveFeign.java | 23 +- .../main/java/feign/RequestInterceptor.java | 14 +- .../java/feign/SynchronousMethodHandler.java | 49 ++-- .../test/java/feign/DefaultContractTest.java | 67 ----- core/src/test/java/feign/FeignTest.java | 233 +++++++----------- core/src/test/java/feign/LoggerTest.java | 2 - .../java/feign/client/DefaultClientTest.java | 28 +-- .../client/TrustingSSLSocketFactory.java | 6 +- .../java/feign/examples/GitHubExample.java | 2 +- dagger.gradle | 178 ------------- gson/README.md | 6 - gson/src/main/java/feign/gson/GsonCodec.java | 37 --- .../src/main/java/feign/gson/GsonDecoder.java | 9 +- .../src/main/java/feign/gson/GsonEncoder.java | 8 +- .../src/main/java/feign/gson/GsonFactory.java | 46 ++++ gson/src/main/java/feign/gson/GsonModule.java | 92 ------- ...GsonModuleTest.java => GsonCodecTest.java} | 100 +++----- .../feign/gson/examples/GitHubExample.java | 7 +- jackson/README.md | 6 - .../java/feign/jackson/JacksonDecoder.java | 9 +- .../java/feign/jackson/JacksonEncoder.java | 9 +- .../java/feign/jackson/JacksonModule.java | 103 -------- ...nModuleTest.java => JacksonCodecTest.java} | 70 +----- .../feign/jackson/examples/GitHubExample.java | 8 +- jaxb/README.md | 8 - .../src/main/java/feign/jaxb/JAXBDecoder.java | 7 +- .../src/main/java/feign/jaxb/JAXBEncoder.java | 5 +- jaxb/src/main/java/feign/jaxb/JAXBModule.java | 66 ----- ...JAXBModuleTest.java => JAXBCodecTest.java} | 57 +---- .../main/java/feign/jaxrs/JAXRSContract.java | 123 +++++++++ .../main/java/feign/jaxrs/JAXRSModule.java | 133 ---------- .../java/feign/jaxrs/JAXRSContractTest.java | 5 +- .../feign/jaxrs/examples/GitHubExample.java | 28 +-- ribbon/README.md | 12 +- .../feign/ribbon/LoadBalancingTarget.java | 2 +- .../main/java/feign/ribbon/RibbonClient.java | 24 +- .../main/java/feign/ribbon/RibbonModule.java | 48 ---- .../java/feign/ribbon/RibbonClientTest.java | 32 +-- sax/src/main/java/feign/sax/SAXDecoder.java | 65 ++--- .../test/java/feign/sax/SAXDecoderTest.java | 34 +-- 48 files changed, 530 insertions(+), 1542 deletions(-) delete mode 100644 dagger.gradle delete mode 100644 gson/src/main/java/feign/gson/GsonCodec.java create mode 100644 gson/src/main/java/feign/gson/GsonFactory.java delete mode 100644 gson/src/main/java/feign/gson/GsonModule.java rename gson/src/test/java/feign/gson/{GsonModuleTest.java => GsonCodecTest.java} (58%) delete mode 100644 jackson/src/main/java/feign/jackson/JacksonModule.java rename jackson/src/test/java/feign/jackson/{JacksonModuleTest.java => JacksonCodecTest.java} (63%) delete mode 100644 jaxb/src/main/java/feign/jaxb/JAXBModule.java rename jaxb/src/test/java/feign/jaxb/{JAXBModuleTest.java => JAXBCodecTest.java} (73%) create mode 100644 jaxrs/src/main/java/feign/jaxrs/JAXRSContract.java delete mode 100644 jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java delete mode 100644 ribbon/src/main/java/feign/ribbon/RibbonModule.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f11e71227..aa2bd3a946 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +### Version 8.0 +* Removes Dagger 1.x Dependency +* Removes support for parameters annotated with `javax.inject.@Named`. Use `feign.@Param` instead. + ### Version 7.1 * Introduces feign.@Param to annotate template parameters. Users must migrate from `javax.inject.@Named` to `feign.@Param` before updating to Feign 8.0. * Supports custom expansion via `@Param(value = "name", expander = CustomExpander.class)` diff --git a/README.md b/README.md index 1d91b4c7f8..985cc64afc 100644 --- a/README.md +++ b/README.md @@ -50,35 +50,14 @@ interface Bank { Bank bank = Feign.builder().decoder(new AccountDecoder()).target(Bank.class, "https://api.examplebank.com"); ``` -For further flexibility, you can use Dagger modules directly. See the `Dagger` section for more details. - -### 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. - -```java -static class ForwardedForInterceptor implements RequestInterceptor { - @Override public void apply(RequestTemplate template) { - template.header("X-Forwarded-For", "origin.host.com"); - } -} -... -Bank bank = Feign.builder().decoder(accountDecoder).requestInterceptor(new ForwardedForInterceptor()).target(Bank.class, "https://api.examplebank.com"); -``` - -Another common example of an interceptor would be authentication, such as using the built-in `BasicAuthRequestInterceptor`. - -```java -Bank bank = Feign.builder().decoder(accountDecoder).requestInterceptor(new BasicAuthRequestInterceptor(username, password)).target(Bank.class, "https://api.examplebank.com"); -``` - ### Multiple Interfaces Feign can produce multiple api interfaces. These are defined as `Target` (default `HardCodedTarget`), which allow for dynamic discovery and decoration of requests prior to execution. For example, the following pattern might decorate each request with the current url and auth token from the identity service. ```java -CloudDNS cloudDNS = Feign.builder().target(new CloudIdentityTarget(user, apiKey)); +Feign feign = Feign.builder().build(); +CloudDNS cloudDNS = feign.target(new CloudIdentityTarget(user, apiKey)); ``` You can find [several examples](https://github.com/Netflix/feign/tree/master/core/src/test/java/feign/examples) in the test tree. Do take time to look at them, as seeing is believing! @@ -87,7 +66,7 @@ You can find [several examples](https://github.com/Netflix/feign/tree/master/cor Feign intends to work well within Netflix and other Open Source communities. Modules are welcome to integrate with your favorite projects! ### Gson -[GsonModule](https://github.com/Netflix/feign/tree/master/gson) adds default encoders and decoders so you get get started with a JSON api. +[Gson](https://github.com/Netflix/feign/tree/master/gson) includes an encoder and decoder you can use with a JSON API. Add `GsonEncoder` and/or `GsonDecoder` to your `Feign.Builder` like so: @@ -100,7 +79,7 @@ GitHub github = Feign.builder() ``` ### Jackson -[JacksonModule](https://github.com/Netflix/feign/tree/master/jackson) adds an encoder and decoder you can use with a JSON API. +[Jackson](https://github.com/Netflix/feign/tree/master/jackson) includes an encoder and decoder you can use with a JSON API. Add `JacksonEncoder` and/or `JacksonDecoder` to your `Feign.Builder` like so: @@ -124,7 +103,7 @@ api = Feign.builder() ``` ### JAXB -[JAXBModule](https://github.com/Netflix/feign/tree/master/jaxb) allows you to encode and decode XML using JAXB. +[JAXB](https://github.com/Netflix/feign/tree/master/jaxb) includes an encoder and decoder you can use with an XML API. Add `JAXBEncoder` and/or `JAXBDecoder` to your `Feign.Builder` like so: @@ -136,7 +115,7 @@ api = Feign.builder() ``` ### JAX-RS -[JAXRSModule](https://github.com/Netflix/feign/tree/master/jaxrs) overrides annotation processing to instead use standard ones supplied by the JAX-RS specification. This is currently targeted at the 1.1 spec. +[JAXRSContract](https://github.com/Netflix/feign/tree/master/jaxrs) overrides annotation processing to instead use standard ones supplied by the JAX-RS specification. This is currently targeted at the 1.1 spec. Here's the example above re-written to use JAX-RS: ```java @@ -147,7 +126,7 @@ interface GitHub { ``` ```java GitHub github = Feign.builder() - .contract(new JAXRSModule.JAXRSContract()) + .contract(new JAXRSContract()) .target(GitHub.class, "https://api.github.com"); ``` ### OkHttp @@ -162,11 +141,12 @@ GitHub github = Feign.builder() ``` ### Ribbon -[RibbonModule](https://github.com/Netflix/feign/tree/master/ribbon) overrides URL resolution of Feign's client, adding smart routing and resiliency capabilities provided by [Ribbon](https://github.com/Netflix/ribbon). +[RibbonClient](https://github.com/Netflix/feign/tree/master/ribbon) overrides URL resolution of Feign's client, adding smart routing and resiliency capabilities provided by [Ribbon](https://github.com/Netflix/ribbon). Integration requires you to pass your ribbon client name as the host part of the url, for example `myAppProd`. ```java -MyService api = Feign.create(MyService.class, "https://myAppProd", new RibbonModule()); +MyService api = Feign.builder().client(new RibbonClient()).target(MyService.class, "https://myAppProd"); + ``` ### SLF4J @@ -206,27 +186,45 @@ GitHub github = Feign.builder() .target(GitHub.class, "https://api.github.com"); ``` -### Advanced usage and Dagger -#### Dagger -Feign can be directly wired into Dagger which keeps things at compile time and Android friendly. As opposed to exposing builders for config, Feign intends users to embed their config in Dagger. +### Advanced usage -Where possible, Feign configuration uses normal Dagger conventions. For example, `RequestInterceptor` bindings are of `Provider.Type.SET`, meaning you can have multiple interceptors. Here's an example of multiple interceptor bindings. +#### Logging +You can log the http messages going to and from the target by setting up a `Logger`. Here's the easiest way to do that: ```java -@Provides(type = SET) RequestInterceptor forwardedForInterceptor() { - return new RequestInterceptor() { - @Override public void apply(RequestTemplate template) { - template.header("X-Forwarded-For", "origin.host.com"); - } - }; -} +GitHub github = Feign.builder() + .decoder(new GsonDecoder()) + .logger(new Logger.JavaLogger().appendToFile("logs/http.log")) + .logLevel(Logger.Level.FULL) + .target(GitHub.class, "https://api.github.com"); +``` -@Provides(type = SET) RequestInterceptor userAgentInterceptor() { - return new RequestInterceptor() { - @Override public void apply(RequestTemplate template) { - template.header("User-Agent", "My Cool Client"); - } - }; +The SLF4JLogger (see above) may also be of interest. + + +#### 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. + +```java +static class ForwardedForInterceptor implements RequestInterceptor { + @Override public void apply(RequestTemplate template) { + template.header("X-Forwarded-For", "origin.host.com"); + } } +... +Bank bank = Feign.builder() + .decoder(accountDecoder) + .requestInterceptor(new ForwardedForInterceptor()) + .target(Bank.class, "https://api.examplebank.com"); +``` + +Another common example of an interceptor would be authentication, such as using the built-in `BasicAuthRequestInterceptor`. + +```java +Bank bank = Feign.builder() + .decoder(accountDecoder) + .requestInterceptor(new BasicAuthRequestInterceptor(username, password)) + .target(Bank.class, "https://api.examplebank.com"); ``` #### Custom Parameter Expansion @@ -237,15 +235,3 @@ for example formatting dates. ```java @RequestLine("GET /?since={date}") Result list(@Param(value = "date", expander = DateToMillis.class) Date date); ``` - -#### Logging -You can log the http messages going to and from the target by setting up a `Logger`. Here's the easiest way to do that: -```java -GitHub github = Feign.builder() - .decoder(new GsonDecoder()) - .logger(new Logger.JavaLogger().appendToFile("logs/http.log")) - .logLevel(Logger.Level.FULL) - .target(GitHub.class, "https://api.github.com"); -``` - -The SLF4JModule (see above) may also be of interest. diff --git a/build.gradle b/build.gradle index d8579d81d2..a4ccddd83a 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,5 @@ subprojects { repositories { jcenter() } - apply from: rootProject.file('dagger.gradle') group = "com.netflix.${githubProjectName}" // TEMPLATE: Set to organization of project } diff --git a/core/build.gradle b/core/build.gradle index 9edfdcb787..8497abb796 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -3,9 +3,8 @@ apply plugin: 'java' sourceCompatibility = 1.6 dependencies { - testCompile 'com.google.code.gson:gson:2.2.4' - testCompile 'com.fasterxml.jackson.core:jackson-databind:2.2.2' testCompile 'junit:junit:4.12' testCompile 'org.assertj:assertj-core:1.7.1' testCompile 'com.squareup.okhttp:mockwebserver:2.2.0' + testCompile 'com.google.code.gson:gson:2.2.4' // for example } diff --git a/core/src/main/java/feign/Client.java b/core/src/main/java/feign/Client.java index 6ba3796f43..d881177bb7 100644 --- a/core/src/main/java/feign/Client.java +++ b/core/src/main/java/feign/Client.java @@ -26,12 +26,10 @@ 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; @@ -55,10 +53,11 @@ public interface Client { Response execute(Request request, Options options) throws IOException; public static class Default implements Client { - private final Lazy sslContextFactory; - private final Lazy hostnameVerifier; + private final SSLSocketFactory sslContextFactory; + private final HostnameVerifier hostnameVerifier; - @Inject public Default(Lazy sslContextFactory, Lazy hostnameVerifier) { + /** Null parameters imply platform defaults. */ + public Default(SSLSocketFactory sslContextFactory, HostnameVerifier hostnameVerifier) { this.sslContextFactory = sslContextFactory; this.hostnameVerifier = hostnameVerifier; } @@ -72,8 +71,12 @@ HttpURLConnection convertAndSend(Request request, Options options) throws IOExce final HttpURLConnection connection = (HttpURLConnection) new URL(request.url()).openConnection(); if (connection instanceof HttpsURLConnection) { HttpsURLConnection sslCon = (HttpsURLConnection) connection; - sslCon.setSSLSocketFactory(sslContextFactory.get()); - sslCon.setHostnameVerifier(hostnameVerifier.get()); + if (sslContextFactory != null) { + sslCon.setSSLSocketFactory(sslContextFactory); + } + if (hostnameVerifier != null) { + sslCon.setHostnameVerifier(hostnameVerifier); + } } connection.setConnectTimeout(options.connectTimeoutMillis()); connection.setReadTimeout(options.readTimeoutMillis()); diff --git a/core/src/main/java/feign/Contract.java b/core/src/main/java/feign/Contract.java index 6be6e49ec1..931b5d1436 100644 --- a/core/src/main/java/feign/Contract.java +++ b/core/src/main/java/feign/Contract.java @@ -15,13 +15,12 @@ */ package feign; -import java.util.LinkedHashMap; -import javax.inject.Named; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.net.URI; import java.util.ArrayList; import java.util.Collection; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -168,10 +167,9 @@ protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[ boolean isHttpAnnotation = false; for (Annotation annotation : annotations) { Class annotationType = annotation.annotationType(); - if (annotationType == Param.class || annotationType == Named.class) { - String name = annotationType == Param.class ? ((Param) annotation).value() : ((Named) annotation).value(); - checkState(emptyToNull(name) != null, - "%s annotation was empty on param %s.", annotationType.getSimpleName(), paramIndex); + if (annotationType == Param.class) { + String name = ((Param) annotation).value(); + checkState(emptyToNull(name) != null, "Param annotation was empty on param %s.", paramIndex); nameParam(data, name, paramIndex); if (annotationType == Param.class) { Class expander = ((Param) annotation).expander(); diff --git a/core/src/main/java/feign/Feign.java b/core/src/main/java/feign/Feign.java index cc5bd597e2..75318d71e3 100644 --- a/core/src/main/java/feign/Feign.java +++ b/core/src/main/java/feign/Feign.java @@ -15,25 +15,16 @@ */ package feign; - -import dagger.ObjectGraph; -import dagger.Provides; import feign.Logger.NoOpLogger; +import feign.ReflectiveFeign.ParseHandlersByName; import feign.Request.Options; import feign.Target.HardCodedTarget; import feign.codec.Decoder; import feign.codec.Encoder; import feign.codec.ErrorDecoder; - -import javax.inject.Inject; -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLSocketFactory; import java.lang.reflect.Method; import java.util.ArrayList; -import java.util.LinkedHashSet; import java.util.List; -import java.util.Set; /** * Feign's purpose is to ease development against http apis that feign @@ -55,80 +46,6 @@ public static Builder builder() { return new Builder(); } - public static T create(Class apiType, String url, Object... modules) { - return create(new HardCodedTarget(apiType, url), modules); - } - - /** - * Shortcut to {@link #newInstance(Target) create} a single {@code targeted} - * http api using {@link ReflectiveFeign reflection}. - */ - public static T create(Target target, Object... modules) { - return create(modules).newInstance(target); - } - - /** - * Returns a {@link ReflectiveFeign reflective} factory for generating - * {@link Target targeted} http apis. - */ - public static Feign create(Object... modules) { - return ObjectGraph.create(modulesForGraph(modules).toArray()).get(Feign.class); - } - - - /** - * Returns an {@link ObjectGraph Dagger ObjectGraph} that can inject a - * {@link ReflectiveFeign reflective} Feign. - */ - public static ObjectGraph createObjectGraph(Object... modules) { - return ObjectGraph.create(modulesForGraph(modules).toArray()); - } - - @SuppressWarnings("rawtypes") - // incomplete as missing Encoder/Decoder - @dagger.Module(injects = {Feign.class, Builder.class}, complete = false, includes = ReflectiveFeign.Module.class) - public static class Defaults { - @Provides Contract contract() { - return new Contract.Default(); - } - - @Provides Logger.Level logLevel() { - return Logger.Level.NONE; - } - - @Provides Logger noOp() { - return new NoOpLogger(); - } - - @Provides Retryer retryer() { - return new Retryer.Default(); - } - - @Provides ErrorDecoder errorDecoder() { - return new ErrorDecoder.Default(); - } - - @Provides Options options() { - return new Options(); - } - - @Provides SSLSocketFactory sslSocketFactory() { - return SSLSocketFactory.class.cast(SSLSocketFactory.getDefault()); - } - - @Provides HostnameVerifier hostnameVerifier() { - return HttpsURLConnection.getDefaultHostnameVerifier(); - } - - @Provides Client httpClient(Client.Default client) { - return client; - } - - @Provides InvocationHandlerFactory invocationHandlerFactory() { - return new InvocationHandlerFactory.Default(); - } - } - /** *
* Configuration keys are formatted as unresolved modulesForGraph(Object... modules) { - List modulesForGraph = new ArrayList(2); - modulesForGraph.add(new Defaults()); - if (modules != null) - for (Object module : modules) - modulesForGraph.add(module); - return modulesForGraph; - } - - @dagger.Module(injects = Feign.class, includes = ReflectiveFeign.Module.class) public static class Builder { - private final Set requestInterceptors = new LinkedHashSet(); - @Inject Logger.Level logLevel; - @Inject Contract contract; - @Inject Client client; - @Inject Retryer retryer; - @Inject Logger logger; - Encoder encoder = new Encoder.Default(); - Decoder decoder = new Decoder.Default(); - @Inject ErrorDecoder errorDecoder; - @Inject Options options; - @Inject InvocationHandlerFactory invocationHandlerFactory; - - Builder() { - ObjectGraph.create(new Defaults()).inject(this); - } + private final List requestInterceptors = new ArrayList(); + private Logger.Level logLevel = Logger.Level.NONE; + private Contract contract = new Contract.Default(); + private Client client = new Client.Default(null, null); + private Retryer retryer = new Retryer.Default(); + private Logger logger = new NoOpLogger(); + private Encoder encoder = new Encoder.Default(); + private Decoder decoder = new Decoder.Default(); + private ErrorDecoder errorDecoder = new ErrorDecoder.Default(); + private Options options = new Options(); + private InvocationHandlerFactory invocationHandlerFactory = new InvocationHandlerFactory.Default(); public Builder logLevel(Logger.Level logLevel) { this.logLevel = logLevel; @@ -262,51 +165,15 @@ public T target(Class apiType, String url) { } public T target(Target target) { - return ObjectGraph.create(this).get(Feign.class).newInstance(target); - } - - @Provides Logger.Level logLevel() { - return logLevel; - } - - @Provides Contract contract() { - return contract; - } - - @Provides Client client() { - return client; - } - - @Provides Retryer retryer() { - return retryer; - } - - @Provides Logger logger() { - return logger; - } - - @Provides Encoder encoder() { - return encoder; - } - - @Provides Decoder decoder() { - return decoder; - } - - @Provides ErrorDecoder errorDecoder() { - return errorDecoder; - } - - @Provides Options options() { - return options; - } - - @Provides(type = Provides.Type.SET_VALUES) Set requestInterceptors() { - return requestInterceptors; + return build().newInstance(target); } - @Provides InvocationHandlerFactory invocationHandlerFactory() { - return invocationHandlerFactory; + public Feign build() { + SynchronousMethodHandler.Factory synchronousMethodHandlerFactory = + new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger, logLevel); + ParseHandlersByName handlersByName = new ParseHandlersByName( contract, options, encoder, decoder, + errorDecoder, synchronousMethodHandlerFactory); + return new ReflectiveFeign(handlersByName, invocationHandlerFactory); } } } diff --git a/core/src/main/java/feign/InvocationHandlerFactory.java b/core/src/main/java/feign/InvocationHandlerFactory.java index cf8080492e..7dabf77ded 100644 --- a/core/src/main/java/feign/InvocationHandlerFactory.java +++ b/core/src/main/java/feign/InvocationHandlerFactory.java @@ -21,6 +21,7 @@ /** Controls reflective method dispatch. */ public interface InvocationHandlerFactory { + /** Like {@link InvocationHandler#invoke(Object, java.lang.reflect.Method, Object[])}, except for a single method. */ interface MethodHandler { Object invoke(Object[] argv) throws Throwable; diff --git a/core/src/main/java/feign/ReflectiveFeign.java b/core/src/main/java/feign/ReflectiveFeign.java index 4b19c84732..541bd5f69d 100644 --- a/core/src/main/java/feign/ReflectiveFeign.java +++ b/core/src/main/java/feign/ReflectiveFeign.java @@ -15,7 +15,6 @@ */ package feign; -import dagger.Provides; import feign.InvocationHandlerFactory.MethodHandler; import feign.Param.Expander; import feign.Request.Options; @@ -24,28 +23,24 @@ import feign.codec.Encoder; import feign.codec.ErrorDecoder; -import javax.inject.Inject; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Collection; -import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.Set; import static feign.Util.checkArgument; import static feign.Util.checkNotNull; -@SuppressWarnings("rawtypes") public class ReflectiveFeign extends Feign { private final ParseHandlersByName targetToHandlersByName; private final InvocationHandlerFactory factory; - @Inject ReflectiveFeign(ParseHandlersByName targetToHandlersByName, InvocationHandlerFactory factory) { + ReflectiveFeign(ParseHandlersByName targetToHandlersByName, InvocationHandlerFactory factory) { this.targetToHandlersByName = targetToHandlersByName; this.factory = factory; } @@ -109,17 +104,6 @@ static class FeignInvocationHandler implements InvocationHandler { } } - @dagger.Module(complete = false, injects = {Feign.class, SynchronousMethodHandler.Factory.class}, library = true) - public static class Module { - @Provides(type = Provides.Type.SET_VALUES) Set noRequestInterceptors() { - return Collections.emptySet(); - } - - @Provides Feign provideFeign(ReflectiveFeign in) { - return in; - } - } - static final class ParseHandlersByName { private final Contract contract; private final Options options; @@ -128,9 +112,8 @@ static final class ParseHandlersByName { private final ErrorDecoder errorDecoder; private final SynchronousMethodHandler.Factory factory; - @SuppressWarnings("unchecked") - @Inject ParseHandlersByName(Contract contract, Options options, Encoder encoder, Decoder decoder, - ErrorDecoder errorDecoder, SynchronousMethodHandler.Factory factory) { + ParseHandlersByName(Contract contract, Options options, Encoder encoder, Decoder decoder, + ErrorDecoder errorDecoder, SynchronousMethodHandler.Factory factory) { this.contract = contract; this.options = options; this.factory = factory; diff --git a/core/src/main/java/feign/RequestInterceptor.java b/core/src/main/java/feign/RequestInterceptor.java index 39b79c60b0..0c4ad016fc 100644 --- a/core/src/main/java/feign/RequestInterceptor.java +++ b/core/src/main/java/feign/RequestInterceptor.java @@ -33,19 +33,7 @@ *
*
Configuration
*
- * {@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. - *
- *
- * For example: - *
- *
- * {@literal @}Provides(Type = SET) RequestInterceptor addTimestamp(TimestampInterceptor in) {
- * return in;
- * }
- * 
+ * {@code RequestInterceptors} are configured via {@link Feign.Builder#requestInterceptors}. *
*
Implementation notes
*
diff --git a/core/src/main/java/feign/SynchronousMethodHandler.java b/core/src/main/java/feign/SynchronousMethodHandler.java index 83c102da45..764636a503 100644 --- a/core/src/main/java/feign/SynchronousMethodHandler.java +++ b/core/src/main/java/feign/SynchronousMethodHandler.java @@ -20,11 +20,8 @@ import feign.codec.DecodeException; import feign.codec.Decoder; import feign.codec.ErrorDecoder; - -import javax.inject.Inject; -import javax.inject.Provider; import java.io.IOException; -import java.util.Set; +import java.util.List; import java.util.concurrent.TimeUnit; import static feign.FeignException.errorExecuting; @@ -37,13 +34,13 @@ final class SynchronousMethodHandler implements MethodHandler { static class Factory { private final Client client; - private final Provider retryer; - private final Set requestInterceptors; + private final Retryer retryer; + private final List requestInterceptors; private final Logger logger; - private final Provider logLevel; + private final Logger.Level logLevel; - @Inject Factory(Client client, Provider retryer, Set requestInterceptors, - Logger logger, Provider logLevel) { + Factory(Client client, Retryer retryer, List requestInterceptors, + Logger logger, Logger.Level logLevel) { this.client = checkNotNull(client, "client"); this.retryer = checkNotNull(retryer, "retryer"); this.requestInterceptors = checkNotNull(requestInterceptors, "requestInterceptors"); @@ -61,18 +58,18 @@ public MethodHandler create(Target target, MethodMetadata md, RequestTemplate private final MethodMetadata metadata; private final Target target; private final Client client; - private final Provider retryer; - private final Set requestInterceptors; + private final Retryer retryer; + private final List requestInterceptors; private final Logger logger; - private final Provider logLevel; + private final Logger.Level logLevel; private final RequestTemplate.Factory buildTemplateFromArgs; private final Options options; private final Decoder decoder; private final ErrorDecoder errorDecoder; - private SynchronousMethodHandler(Target target, Client client, Provider retryer, - Set requestInterceptors, Logger logger, - Provider logLevel, MethodMetadata metadata, + private SynchronousMethodHandler(Target target, Client client, Retryer retryer, + List requestInterceptors, Logger logger, + Logger.Level logLevel, MethodMetadata metadata, RequestTemplate.Factory buildTemplateFromArgs, Options options, Decoder decoder, ErrorDecoder errorDecoder) { this.target = checkNotNull(target, "target"); @@ -90,14 +87,14 @@ private SynchronousMethodHandler(Target target, Client client, Provider target, Client client, Provider= 200 && response.status() < 300) { if (Response.class == metadata.returnType()) { @@ -144,8 +141,8 @@ Object executeAndDecode(RequestTemplate template) throws Throwable { throw errorDecoder.decode(metadata.configKey(), response); } } catch (IOException e) { - if (logLevel.get() != Logger.Level.NONE) { - logger.logIOException(metadata.configKey(), logLevel.get(), e, elapsedTime); + if (logLevel != Logger.Level.NONE) { + logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime); } throw errorReading(request, response, e); } finally { diff --git a/core/src/test/java/feign/DefaultContractTest.java b/core/src/test/java/feign/DefaultContractTest.java index 1906e75bea..739d9884f8 100644 --- a/core/src/test/java/feign/DefaultContractTest.java +++ b/core/src/test/java/feign/DefaultContractTest.java @@ -19,7 +19,6 @@ import java.net.URI; import java.util.Date; import java.util.List; -import javax.inject.Named; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -264,70 +263,4 @@ class DateToMillis implements Param.Expander { assertThat(md.indexToExpanderClass()) .containsExactly(entry(0, DateToMillis.class)); } - - // TODO: remove all of below in 8.x - - interface WithPathAndQueryParamsAnnotatedWithNamed { - @RequestLine("GET /domains/{domainId}/records?name={name}&type={type}") - Response recordsByNameAndType(@Named("domainId") int id, @Named("name") String nameFilter, - @Named("type") String typeFilter); - } - - @Test public void pathAndQueryParamsAnnotatedWithNamed() throws Exception { - MethodMetadata md = contract.parseAndValidatateMetadata(WithPathAndQueryParamsAnnotatedWithNamed.class.getDeclaredMethod - ("recordsByNameAndType", int.class, String.class, String.class)); - - assertThat(md.template()) - .hasQueries(entry("name", asList("{name}")), entry("type", asList("{type}"))); - - assertThat(md.indexToName()).containsExactly( - entry(0, asList("domainId")), - entry(1, asList("name")), - entry(2, asList("type")) - ); - } - - interface FormParamsAnnotatedWithNamed { - @RequestLine("POST /") - @Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D") - void login( - @Named("customer_name") String customer, - @Named("user_name") String user, @Named("password") String password); - } - - @Test public void bodyWithTemplateAnnotatedWithNamed() throws Exception { - MethodMetadata md = contract.parseAndValidatateMetadata(FormParamsAnnotatedWithNamed.class.getDeclaredMethod("login", String.class, - String.class, String.class)); - - assertThat(md.template()) - .hasBodyTemplate("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D"); - } - - @Test public void formParamsAnnotatedWithNamedParseIntoIndexToName() throws Exception { - MethodMetadata md = contract.parseAndValidatateMetadata(FormParamsAnnotatedWithNamed.class.getDeclaredMethod("login", String.class, - String.class, String.class)); - - assertThat(md.formParams()) - .containsExactly("customer_name", "user_name", "password"); - - assertThat(md.indexToName()).containsExactly( - entry(0, asList("customer_name")), - entry(1, asList("user_name")), - entry(2, asList("password")) - ); - } - - interface HeaderParamsAnnotatedWithNamed { - @RequestLine("POST /") - @Headers("Auth-Token: {Auth-Token}") void logout(@Named("Auth-Token") String token); - } - - @Test public void headerParamsAnnotatedWithNamedParseIntoIndexToName() throws Exception { - MethodMetadata md = contract.parseAndValidatateMetadata(HeaderParamsAnnotatedWithNamed.class.getDeclaredMethod("logout", String.class)); - - assertThat(md.template()).hasHeaders(entry("Auth-Token", asList("{Auth-Token}"))); - - assertThat(md.indexToName()) - .containsExactly(entry(0, asList("Auth-Token"))); - } } diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index fbf0f20e40..939190a509 100644 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -19,8 +19,6 @@ import com.squareup.okhttp.mockwebserver.MockResponse; import com.squareup.okhttp.mockwebserver.SocketPolicy; import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule; -import dagger.Module; -import dagger.Provides; import feign.Target.HardCodedTarget; import feign.codec.Decoder; import feign.codec.Encoder; @@ -33,19 +31,15 @@ import java.util.Date; import java.util.List; import java.util.Map; -import javax.inject.Singleton; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import static dagger.Provides.Type.SET; import static feign.Util.UTF_8; import static feign.assertj.MockWebServerAssertions.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -// unbound wildcards are not currently injectable in dagger. -@SuppressWarnings("rawtypes") public class FeignTest { @Rule public final ExpectedException thrown = ExpectedException.none(); @Rule public final MockWebServerRule server = new MockWebServerRule(); @@ -78,32 +72,12 @@ class DateToMillis implements Param.Expander { return String.valueOf(((Date) value).getTime()); } } - - @dagger.Module(injects = Feign.class, addsTo = Feign.Defaults.class) - static class Module { - @Provides Decoder defaultDecoder() { - return new Decoder.Default(); - } - - @Provides Encoder defaultEncoder() { - return new Encoder() { - @Override public void encode(Object object, RequestTemplate template) { - if (object instanceof Map) { - template.body(new Gson().toJson(object)); - } else { - template.body(object.toString()); - } - } - }; - } - } } @Test public void iterableQueryParams() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("foo")); - TestInterface api = - Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module()); + TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); api.queryParams("user", Arrays.asList("apple", "pear")); @@ -119,12 +93,10 @@ interface OtherTestInterface { @RequestLine("POST /") void binaryRequestBody(byte[] contents); } - @Test - public void postTemplateParamsResolve() throws IOException, InterruptedException { + @Test public void postTemplateParamsResolve() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("foo")); - - TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), - new TestInterface.Module()); + + TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); api.login("netflix", "denominator", "password"); @@ -132,24 +104,20 @@ public void postTemplateParamsResolve() throws IOException, InterruptedException .hasBody("{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}"); } - @Test - public void responseCoercesToStringBody() throws IOException, InterruptedException { + @Test public void responseCoercesToStringBody() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("foo")); - TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), - new TestInterface.Module()); + TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); Response response = api.response(); assertTrue(response.body().isRepeatable()); assertEquals("foo", response.body().toString()); } - @Test - public void postFormParams() throws IOException, InterruptedException { + @Test public void postFormParams() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("foo")); - TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), - new TestInterface.Module()); + TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); api.form("netflix", "denominator", "password"); @@ -157,12 +125,10 @@ public void postFormParams() throws IOException, InterruptedException { .hasBody("{\"customer_name\":\"netflix\",\"user_name\":\"denominator\",\"password\":\"password\"}"); } - @Test - public void postBodyParam() throws IOException, InterruptedException { + @Test public void postBodyParam() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("foo")); - TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), - new TestInterface.Module()); + TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); api.body(Arrays.asList("netflix", "denominator", "password")); @@ -171,12 +137,10 @@ public void postBodyParam() throws IOException, InterruptedException { .hasBody("[netflix, denominator, password]"); } - @Test - public void postGZIPEncodedBodyParam() throws IOException, InterruptedException { + @Test public void postGZIPEncodedBodyParam() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("foo")); - TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), - new TestInterface.Module()); + TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); api.gzipBody(Arrays.asList("netflix", "denominator", "password")); @@ -185,23 +149,18 @@ public void postGZIPEncodedBodyParam() throws IOException, InterruptedException .hasGzippedBody("[netflix, denominator, password]".getBytes(UTF_8)); } - @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 { + @Test public void singleInterceptor() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("foo")); - - TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), - new TestInterface.Module(), new ForwardedForInterceptor()); + + TestInterface api = new TestInterfaceBuilder() + .requestInterceptor(new ForwardedForInterceptor()) + .target("http://localhost:" + server.getPort()); api.post(); @@ -209,35 +168,29 @@ public void singleInterceptor() throws IOException, InterruptedException { .hasHeaders("X-Forwarded-For: origin.host.com"); } - @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 { + @Test public void multipleInterceptor() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("foo")); - TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), - new TestInterface.Module(), new ForwardedForInterceptor(), new UserAgentInterceptor()); + TestInterface api = new TestInterfaceBuilder() + .requestInterceptor(new ForwardedForInterceptor()) + .requestInterceptor(new UserAgentInterceptor()) + .target("http://localhost:" + server.getPort()); api.post(); - assertThat(server.takeRequest()) - .hasHeaders("X-Forwarded-For: origin.host.com", "User-Agent: Feign"); + assertThat(server.takeRequest()).hasHeaders("X-Forwarded-For: origin.host.com", "User-Agent: Feign"); } @Test public void customExpander() throws Exception { server.enqueue(new MockResponse()); - TestInterface api = - Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module()); + TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); api.expand(new Date(1234l)); @@ -251,30 +204,21 @@ public void multipleInterceptor() throws IOException, InterruptedException { Feign.configKey(TestInterface.class.getDeclaredMethod("uriParam", String.class, URI.class, String.class))); } - @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); - } - - }; + static class IllegalArgumentExceptionOn404 extends 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 - public void canOverrideErrorDecoder() throws IOException, InterruptedException { + @Test public void canOverrideErrorDecoder() throws IOException, InterruptedException { server.enqueue(new MockResponse().setResponseCode(404).setBody("foo")); thrown.expect(IllegalArgumentException.class); thrown.expectMessage("zone not found"); - TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), - new IllegalArgumentExceptionOn404()); + TestInterface api = new TestInterfaceBuilder() + .errorDecoder(new IllegalArgumentExceptionOn404()) + .target("http://localhost:" + server.getPort()); api.post(); } @@ -283,83 +227,58 @@ public void canOverrideErrorDecoder() throws IOException, InterruptedException { server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); server.enqueue(new MockResponse().setBody("success!")); - TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), - new TestInterface.Module()); + TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); api.post(); assertEquals(2, server.getRequestCount()); } - @dagger.Module(overrides = true, library = true, includes = TestInterface.Module.class) - static class DecodeFail { - @Provides Decoder decoder() { - return new Decoder() { - @Override - public Object decode(Response response, Type type) { - return "fail"; - } - }; - } - } - @Test public void overrideTypeSpecificDecoder() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("success!")); - - TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), - new DecodeFail()); + + TestInterface api = new TestInterfaceBuilder() + .decoder(new Decoder() { + @Override public Object decode(Response response, Type type) { + return "fail"; + } + }).target("http://localhost:" + server.getPort()); assertEquals(api.post(), "fail"); } - @dagger.Module(overrides = true, library = true, includes = TestInterface.Module.class) - static class RetryableExceptionOnRetry { - @Provides Decoder decoder() { - return new StringDecoder() { - @Override - public Object decode(Response response, Type type) throws IOException, FeignException { - String string = super.decode(response, type).toString(); - 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 { + @Test public void retryableExceptionInDecoder() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("retry!")); server.enqueue(new MockResponse().setBody("success!")); - - TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), - new RetryableExceptionOnRetry()); + + TestInterface api = new TestInterfaceBuilder() + .decoder(new StringDecoder() { + @Override public Object decode(Response response, Type type) throws IOException { + String string = super.decode(response, type).toString(); + if ("retry!".equals(string)) throw new RetryableException(string, null); + return string; + } + }).target("http://localhost:" + server.getPort()); assertEquals(api.post(), "success!"); assertEquals(2, server.getRequestCount()); } - @dagger.Module(overrides = true, library = true, includes = TestInterface.Module.class) - static class IOEOnDecode { - @Provides Decoder decoder() { - return new Decoder() { - @Override - public Object decode(Response response, Type type) throws IOException { - throw new IOException("error reading response"); - } - }; - } - } - @Test - public void doesntRetryAfterResponseIsSent() throws IOException, InterruptedException { + @Test public void doesntRetryAfterResponseIsSent() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("success!")); thrown.expect(FeignException.class); thrown.expectMessage("error reading response POST http://"); - TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new IOEOnDecode()); + TestInterface api = new TestInterfaceBuilder() + .decoder(new Decoder() { + @Override public Object decode(Response response, Type type) throws IOException { + throw new IOException("error reading response"); + } + }).target("http://localhost:" + server.getPort()); try { api.post(); @@ -424,4 +343,42 @@ public void doesntRetryAfterResponseIsSent() throws IOException, InterruptedExce assertThat(server.takeRequest()) .hasBody(expectedRequest); } + + static final class TestInterfaceBuilder { + private final Feign.Builder delegate = new Feign.Builder() + .decoder(new Decoder.Default()) + .encoder(new Encoder() { + @Override public void encode(Object object, RequestTemplate template) { + if (object instanceof Map) { + template.body(new Gson().toJson(object)); + } else { + template.body(object.toString()); + } + } + }); + + TestInterfaceBuilder requestInterceptor(RequestInterceptor requestInterceptor) { + delegate.requestInterceptor(requestInterceptor); + return this; + } + + TestInterfaceBuilder client(Client client) { + delegate.client(client); + return this; + } + + TestInterfaceBuilder decoder(Decoder decoder) { + delegate.decoder(decoder); + return this; + } + + TestInterfaceBuilder errorDecoder(ErrorDecoder errorDecoder) { + delegate.errorDecoder(errorDecoder); + return this; + } + + TestInterface target(String url) { + return delegate.target(TestInterface.class, url); + } + } } diff --git a/core/src/test/java/feign/LoggerTest.java b/core/src/test/java/feign/LoggerTest.java index 8748fc279c..69aff9aabc 100644 --- a/core/src/test/java/feign/LoggerTest.java +++ b/core/src/test/java/feign/LoggerTest.java @@ -192,7 +192,6 @@ public UnknownHostEmitsTest(Level logLevel, List expectedMessages) { } @Test public void unknownHostEmits() throws IOException, InterruptedException { - SendsStuff api = Feign.builder() .logger(logger) .logLevel(logLevel) @@ -232,7 +231,6 @@ public RetryEmitsTest(Level logLevel, List expectedMessages) { } @Test public void retryEmits() throws IOException, InterruptedException { - thrown.expect(FeignException.class); SendsStuff api = Feign.builder() diff --git a/core/src/test/java/feign/client/DefaultClientTest.java b/core/src/test/java/feign/client/DefaultClientTest.java index c100375c7f..ba59b0a089 100644 --- a/core/src/test/java/feign/client/DefaultClientTest.java +++ b/core/src/test/java/feign/client/DefaultClientTest.java @@ -18,7 +18,6 @@ import com.squareup.okhttp.mockwebserver.MockResponse; import com.squareup.okhttp.mockwebserver.SocketPolicy; import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule; -import dagger.Lazy; import feign.Client; import feign.Feign; import feign.FeignException; @@ -29,9 +28,7 @@ import java.io.IOException; import java.net.ProtocolException; import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLSession; -import javax.net.ssl.SSLSocketFactory; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -98,15 +95,7 @@ interface TestInterface { api.patch(); } - Client trustSSLSockets = new Client.Default(new Lazy() { - @Override public SSLSocketFactory get() { - return TrustingSSLSocketFactory.get(); - } - }, new Lazy() { - @Override public HostnameVerifier get() { - return HttpsURLConnection.getDefaultHostnameVerifier(); - } - }); + Client trustSSLSockets = new Client.Default(TrustingSSLSocketFactory.get(), null); @Test public void canOverrideSSLSocketFactory() throws IOException, InterruptedException { server.get().useHttps(TrustingSSLSocketFactory.get("localhost"), false); @@ -119,18 +108,9 @@ interface TestInterface { api.post("foo"); } - Client disableHostnameVerification = new Client.Default(new Lazy() { - @Override public SSLSocketFactory get() { - return TrustingSSLSocketFactory.get(); - } - }, new Lazy() { - @Override public HostnameVerifier get() { - return new HostnameVerifier() { - @Override - public boolean verify(String s, SSLSession sslSession) { - return true; - } - }; + Client disableHostnameVerification = new Client.Default(TrustingSSLSocketFactory.get(), new HostnameVerifier() { + @Override public boolean verify(String s, SSLSession sslSession) { + return true; } }); diff --git a/core/src/test/java/feign/client/TrustingSSLSocketFactory.java b/core/src/test/java/feign/client/TrustingSSLSocketFactory.java index adbddcb639..b67225bbba 100644 --- a/core/src/test/java/feign/client/TrustingSSLSocketFactory.java +++ b/core/src/test/java/feign/client/TrustingSSLSocketFactory.java @@ -36,8 +36,6 @@ 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. */ @@ -69,7 +67,7 @@ private TrustingSSLSocketFactory(String serverAlias) { sc.init(new KeyManager[]{this}, new TrustManager[]{this}, new SecureRandom()); this.delegate = sc.getSocketFactory(); } catch (Exception e) { - throw propagate(e); + throw new RuntimeException(e); } this.serverAlias = serverAlias; if (serverAlias.isEmpty()) { @@ -82,7 +80,7 @@ private TrustingSSLSocketFactory(String serverAlias) { Certificate[] rawChain = keyStore.getCertificateChain(serverAlias); this.certificateChain = Arrays.copyOf(rawChain, rawChain.length, X509Certificate[].class); } catch (Exception e) { - throw propagate(e); + throw new RuntimeException(e); } } } diff --git a/core/src/test/java/feign/examples/GitHubExample.java b/core/src/test/java/feign/examples/GitHubExample.java index 7b0d191020..71d7b04ff4 100644 --- a/core/src/test/java/feign/examples/GitHubExample.java +++ b/core/src/test/java/feign/examples/GitHubExample.java @@ -48,9 +48,9 @@ static class Contributor { public static void main(String... args) { GitHub github = Feign.builder() + .decoder(new GsonDecoder()) .logger(new Logger.ErrorLogger()) .logLevel(Logger.Level.BASIC) - .decoder(new GsonDecoder()) .target(GitHub.class, "https://api.github.com"); System.out.println("Let's fetch and print a list of the contributors to this library."); diff --git a/dagger.gradle b/dagger.gradle deleted file mode 100644 index 3217a6e3e0..0000000000 --- a/dagger.gradle +++ /dev/null @@ -1,178 +0,0 @@ -// 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.2.2" - } -} - -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 - } - } - } - } -} - diff --git a/gson/README.md b/gson/README.md index bc6a476887..37c05e0c77 100644 --- a/gson/README.md +++ b/gson/README.md @@ -11,9 +11,3 @@ GitHub github = Feign.builder() .decoder(new GsonDecoder()) .target(GitHub.class, "https://api.github.com"); ``` - -Or add them to your Dagger object graph like so: - -```java -GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule()); -``` diff --git a/gson/src/main/java/feign/gson/GsonCodec.java b/gson/src/main/java/feign/gson/GsonCodec.java deleted file mode 100644 index b6ef12be1e..0000000000 --- a/gson/src/main/java/feign/gson/GsonCodec.java +++ /dev/null @@ -1,37 +0,0 @@ -package feign.gson; - -import com.google.gson.Gson; -import feign.RequestTemplate; -import feign.Response; -import feign.codec.Decoder; -import feign.codec.Encoder; - -import javax.inject.Inject; -import java.io.IOException; -import java.lang.reflect.Type; - -/** - * @deprecated use {@link GsonEncoder} and {@link GsonDecoder} instead - */ -@Deprecated -public class GsonCodec implements Encoder, Decoder { - private final GsonEncoder encoder; - private final GsonDecoder decoder; - - public GsonCodec() { - this(new Gson()); - } - - @Inject public GsonCodec(Gson gson) { - this.encoder = new GsonEncoder(gson); - this.decoder = new GsonDecoder(gson); - } - - @Override public void encode(Object object, RequestTemplate template) { - encoder.encode(object, template); - } - - @Override public Object decode(Response response, Type type) throws IOException { - return decoder.decode(response, type); - } -} diff --git a/gson/src/main/java/feign/gson/GsonDecoder.java b/gson/src/main/java/feign/gson/GsonDecoder.java index 66df54ea85..0a01cc959c 100644 --- a/gson/src/main/java/feign/gson/GsonDecoder.java +++ b/gson/src/main/java/feign/gson/GsonDecoder.java @@ -17,20 +17,25 @@ import com.google.gson.Gson; import com.google.gson.JsonIOException; +import com.google.gson.TypeAdapter; import feign.Response; import feign.codec.Decoder; - import java.io.IOException; import java.io.Reader; import java.lang.reflect.Type; +import java.util.Collections; import static feign.Util.ensureClosed; public class GsonDecoder implements Decoder { private final Gson gson; + public GsonDecoder(Iterable> adapters) { + this(GsonFactory.create(adapters)); + } + public GsonDecoder() { - this(new Gson()); + this(Collections.>emptyList()); } public GsonDecoder(Gson gson) { diff --git a/gson/src/main/java/feign/gson/GsonEncoder.java b/gson/src/main/java/feign/gson/GsonEncoder.java index 4bee8df58b..57e1b54ca8 100644 --- a/gson/src/main/java/feign/gson/GsonEncoder.java +++ b/gson/src/main/java/feign/gson/GsonEncoder.java @@ -16,14 +16,20 @@ package feign.gson; import com.google.gson.Gson; +import com.google.gson.TypeAdapter; import feign.RequestTemplate; import feign.codec.Encoder; +import java.util.Collections; public class GsonEncoder implements Encoder { private final Gson gson; + public GsonEncoder(Iterable> adapters) { + this(GsonFactory.create(adapters)); + } + public GsonEncoder() { - this(new Gson()); + this(Collections.>emptyList()); } public GsonEncoder(Gson gson) { diff --git a/gson/src/main/java/feign/gson/GsonFactory.java b/gson/src/main/java/feign/gson/GsonFactory.java new file mode 100644 index 0000000000..7685b96b28 --- /dev/null +++ b/gson/src/main/java/feign/gson/GsonFactory.java @@ -0,0 +1,46 @@ +/* + * 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.gson; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.TypeAdapter; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.util.Map; + +import static feign.Util.resolveLastTypeParameter; + +final class GsonFactory { + + /** + * Registers type adapters by implicit type. Adds one to read numbers in a + * {@code Map} as Integers. + */ + static Gson create(Iterable> adapters) { + GsonBuilder builder = new GsonBuilder().setPrettyPrinting(); + builder.registerTypeAdapter(new TypeToken>() { + }.getType(), new DoubleToIntMapTypeAdapter()); + for (TypeAdapter adapter : adapters) { + Type type = resolveLastTypeParameter(adapter.getClass(), TypeAdapter.class); + builder.registerTypeAdapter(type, adapter); + } + return builder.create(); + } + + private GsonFactory() { + } +} diff --git a/gson/src/main/java/feign/gson/GsonModule.java b/gson/src/main/java/feign/gson/GsonModule.java deleted file mode 100644 index 79093101f7..0000000000 --- a/gson/src/main/java/feign/gson/GsonModule.java +++ /dev/null @@ -1,92 +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.gson; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.TypeAdapter; -import dagger.Provides; -import feign.Feign; -import feign.codec.Decoder; -import feign.codec.Encoder; - -import javax.inject.Singleton; -import java.lang.reflect.Type; -import java.util.Collections; -import java.util.Set; - -import static feign.Util.resolveLastTypeParameter; - -/** - *

Custom type adapters

- *
- * In order to specify custom json parsing, - * {@code Gson} supports {@link TypeAdapter type adapters}. This module adds one - * to read numbers in a {@code Map} as Integers. You can - * customize further by adding additional set bindings to the raw type - * {@code TypeAdapter}. - *

- *
- * Here's an example of adding a custom json type adapter. - *

- *

- * @Provides(type = Provides.Type.SET)
- * TypeAdapter upperZone() {
- *     return new TypeAdapter<Zone>() {
- *
- *         @Override
- *         public void write(JsonWriter out, Zone value) throws IOException {
- *             throw new IllegalArgumentException();
- *         }
- *
- *         @Override
- *         public Zone read(JsonReader in) throws IOException {
- *             in.beginObject();
- *             Zone zone = new Zone();
- *             while (in.hasNext()) {
- *                 zone.put(in.nextName(), in.nextString().toUpperCase());
- *             }
- *             in.endObject();
- *             return zone;
- *         }
- *     };
- * }
- * 
- */ -@dagger.Module(injects = Feign.class, addsTo = Feign.Defaults.class) -public final class GsonModule { - - @Provides Encoder encoder(Gson gson) { - return new GsonEncoder(gson); - } - - @Provides Decoder decoder(Gson gson) { - return new GsonDecoder(gson); - } - - @Provides @Singleton Gson gson(Set adapters) { - GsonBuilder builder = new GsonBuilder().setPrettyPrinting(); - for (TypeAdapter adapter : adapters) { - Type type = resolveLastTypeParameter(adapter.getClass(), TypeAdapter.class); - builder.registerTypeAdapter(type, adapter); - } - return builder.create(); - } - - @Provides(type = Provides.Type.SET_VALUES) Set noDefaultTypeAdapters() { - return Collections.emptySet(); - } -} diff --git a/gson/src/test/java/feign/gson/GsonModuleTest.java b/gson/src/test/java/feign/gson/GsonCodecTest.java similarity index 58% rename from gson/src/test/java/feign/gson/GsonModuleTest.java rename to gson/src/test/java/feign/gson/GsonCodecTest.java index bf6e1ada25..dab2824db1 100644 --- a/gson/src/test/java/feign/gson/GsonModuleTest.java +++ b/gson/src/test/java/feign/gson/GsonCodecTest.java @@ -19,13 +19,8 @@ import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; -import dagger.Module; -import dagger.ObjectGraph; -import dagger.Provides; import feign.RequestTemplate; import feign.Response; -import feign.codec.Decoder; -import feign.codec.Encoder; import java.io.IOException; import java.util.Arrays; import java.util.Collection; @@ -34,7 +29,6 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; -import javax.inject.Inject; import org.junit.Test; import static feign.Util.UTF_8; @@ -42,35 +36,14 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; -public class GsonModuleTest { - @Module(includes = GsonModule.class, injects = EncoderAndDecoderBindings.class) - static class EncoderAndDecoderBindings { - @Inject Encoder encoder; - @Inject Decoder decoder; - } - - @Test public void providesEncoderDecoder() throws Exception { - EncoderAndDecoderBindings bindings = new EncoderAndDecoderBindings(); - ObjectGraph.create(bindings).inject(bindings); - - assertEquals(GsonEncoder.class, bindings.encoder.getClass()); - assertEquals(GsonDecoder.class, bindings.decoder.getClass()); - } - - @Module(includes = GsonModule.class, injects = EncoderBindings.class) - static class EncoderBindings { - @Inject Encoder encoder; - } +public class GsonCodecTest { @Test public void encodesMapObjectNumericalValuesAsInteger() throws Exception { - EncoderBindings bindings = new EncoderBindings(); - ObjectGraph.create(bindings).inject(bindings); - Map map = new LinkedHashMap(); map.put("foo", 1); RequestTemplate template = new RequestTemplate(); - bindings.encoder.encode(map, template); + new GsonEncoder().encode(map, template); assertThat(template).hasBody("" // + "{\n" // @@ -78,17 +51,24 @@ static class EncoderBindings { + "}"); } - @Test public void encodesFormParams() throws Exception { + @Test public void decodesMapObjectNumericalValuesAsInteger() throws Exception { + Map map = new LinkedHashMap(); + map.put("foo", 1); - EncoderBindings bindings = new EncoderBindings(); - ObjectGraph.create(bindings).inject(bindings); + Response response = + Response.create(200, "OK", Collections.>emptyMap(), "{\"foo\": 1}", UTF_8); + assertEquals(new GsonDecoder().decode(response, new TypeToken>() { + }.getType()), map); + } + + @Test public void encodesFormParams() throws Exception { Map form = new LinkedHashMap(); form.put("foo", 1); form.put("bar", Arrays.asList(2, 3)); RequestTemplate template = new RequestTemplate(); - bindings.encoder.encode(form, template); + new GsonEncoder().encode(form, template); assertThat(template).hasBody("" // + "{\n" // @@ -118,14 +98,7 @@ static class Zone extends LinkedHashMap { private static final long serialVersionUID = 1L; } - @Module(includes = GsonModule.class, injects = DecoderBindings.class) - static class DecoderBindings { - @Inject Decoder decoder; - } - @Test public void decodes() throws Exception { - DecoderBindings bindings = new DecoderBindings(); - ObjectGraph.create(bindings).inject(bindings); List zones = new LinkedList(); zones.add(new Zone("denominator.io.")); @@ -133,16 +106,13 @@ static class DecoderBindings { Response response = Response.create(200, "OK", Collections.>emptyMap(), zonesJson, UTF_8); - assertEquals(zones, bindings.decoder.decode(response, new TypeToken>() { + assertEquals(zones, new GsonDecoder().decode(response, new TypeToken>() { }.getType())); } @Test public void nullBodyDecodesToNull() throws Exception { - DecoderBindings bindings = new DecoderBindings(); - ObjectGraph.create(bindings).inject(bindings); - Response response = Response.create(204, "OK", Collections.>emptyMap(), (byte[]) null); - assertNull(bindings.decoder.decode(response, String.class)); + assertNull(new GsonDecoder().decode(response, String.class)); } private String zonesJson = ""// @@ -156,33 +126,25 @@ static class DecoderBindings { + " }\n"// + "]\n"; - @Module(includes = GsonModule.class, injects = CustomTypeAdapter.class) - static class CustomTypeAdapter { - @Provides(type = Provides.Type.SET) TypeAdapter upperZone() { - return new TypeAdapter() { - - @Override public void write(JsonWriter out, Zone value) throws IOException { - throw new IllegalArgumentException(); - } - - @Override public Zone read(JsonReader in) throws IOException { - in.beginObject(); - Zone zone = new Zone(); - while (in.hasNext()) { - zone.put(in.nextName(), in.nextString().toUpperCase()); - } - in.endObject(); - return zone; - } - }; + final TypeAdapter upperZone = new TypeAdapter() { + + @Override public void write(JsonWriter out, Zone value) throws IOException { + throw new IllegalArgumentException(); } - @Inject Decoder decoder; - } + @Override public Zone read(JsonReader in) throws IOException { + in.beginObject(); + Zone zone = new Zone(); + while (in.hasNext()) { + zone.put(in.nextName(), in.nextString().toUpperCase()); + } + in.endObject(); + return zone; + } + }; @Test public void customDecoder() throws Exception { - CustomTypeAdapter bindings = new CustomTypeAdapter(); - ObjectGraph.create(bindings).inject(bindings); + GsonDecoder decoder = new GsonDecoder(Arrays.>asList(upperZone)); List zones = new LinkedList(); zones.add(new Zone("DENOMINATOR.IO.")); @@ -190,7 +152,7 @@ static class CustomTypeAdapter { Response response = Response.create(200, "OK", Collections.>emptyMap(), zonesJson, UTF_8); - assertEquals(zones, bindings.decoder.decode(response, new TypeToken>() { + assertEquals(zones, decoder.decode(response, new TypeToken>() { }.getType())); } } diff --git a/gson/src/test/java/feign/gson/examples/GitHubExample.java b/gson/src/test/java/feign/gson/examples/GitHubExample.java index 6d41f7007f..7526bdffb6 100644 --- a/gson/src/test/java/feign/gson/examples/GitHubExample.java +++ b/gson/src/test/java/feign/gson/examples/GitHubExample.java @@ -19,7 +19,6 @@ import feign.Param; import feign.RequestLine; import feign.gson.GsonDecoder; - import java.util.List; /** @@ -37,8 +36,10 @@ static class Contributor { int contributions; } - public static void main(String... args) throws InterruptedException { - GitHub github = Feign.builder().decoder(new GsonDecoder()).target(GitHub.class, "https://api.github.com"); + public static void main(String... args) { + GitHub github = Feign.builder() + .decoder(new GsonDecoder()) + .target(GitHub.class, "https://api.github.com"); System.out.println("Let's fetch and print a list of the contributors to this library."); List contributors = github.contributors("netflix", "feign"); diff --git a/jackson/README.md b/jackson/README.md index a6b8f0fcdc..8be632779e 100644 --- a/jackson/README.md +++ b/jackson/README.md @@ -25,9 +25,3 @@ GitHub github = Feign.builder() .decoder(new JacksonDecoder(mapper)) .target(GitHub.class, "https://api.github.com"); ``` - -Alternatively, you can add the encoder and decoder to your Dagger object graph using the provided `JacksonModule` like so: - -```java -GitHub github = Feign.create(GitHub.class, "https://api.github.com", new JacksonModule()); -``` diff --git a/jackson/src/main/java/feign/jackson/JacksonDecoder.java b/jackson/src/main/java/feign/jackson/JacksonDecoder.java index f0734d3768..ffeabce5b6 100644 --- a/jackson/src/main/java/feign/jackson/JacksonDecoder.java +++ b/jackson/src/main/java/feign/jackson/JacksonDecoder.java @@ -16,6 +16,7 @@ package feign.jackson; import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.RuntimeJsonMappingException; import feign.Response; @@ -24,12 +25,18 @@ import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Type; +import java.util.Collections; public class JacksonDecoder implements Decoder { private final ObjectMapper mapper; public JacksonDecoder() { - this(new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)); + this(Collections.emptyList()); + } + + public JacksonDecoder(Iterable modules) { + this(new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .registerModules(modules)); } public JacksonDecoder(ObjectMapper mapper) { diff --git a/jackson/src/main/java/feign/jackson/JacksonEncoder.java b/jackson/src/main/java/feign/jackson/JacksonEncoder.java index 1cc6895f2b..2d0353f931 100644 --- a/jackson/src/main/java/feign/jackson/JacksonEncoder.java +++ b/jackson/src/main/java/feign/jackson/JacksonEncoder.java @@ -17,19 +17,26 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; +import java.util.Collections; public class JacksonEncoder implements Encoder { private final ObjectMapper mapper; public JacksonEncoder() { + this(Collections.emptyList()); + } + + public JacksonEncoder(Iterable modules) { this(new ObjectMapper() .setSerializationInclusion(JsonInclude.Include.NON_NULL) - .configure(SerializationFeature.INDENT_OUTPUT, true)); + .configure(SerializationFeature.INDENT_OUTPUT, true) + .registerModules(modules)); } public JacksonEncoder(ObjectMapper mapper) { diff --git a/jackson/src/main/java/feign/jackson/JacksonModule.java b/jackson/src/main/java/feign/jackson/JacksonModule.java deleted file mode 100644 index 7826118afa..0000000000 --- a/jackson/src/main/java/feign/jackson/JacksonModule.java +++ /dev/null @@ -1,103 +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.jackson; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.Module; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import dagger.Provides; -import feign.Feign; -import feign.codec.Decoder; -import feign.codec.Encoder; - -import javax.inject.Singleton; -import java.util.Collections; -import java.util.Set; - -/** - *

Custom serializers/deserializers

- *
- * In order to specify custom json parsing, Jackson's {@code ObjectMapper} supports {@link JsonSerializer serializers} - * and {@link JsonDeserializer deserializers}, which can be bundled together in {@link Module modules}. - *

- *
- * Here's an example of adding a custom module. - *

- *

- * public class ObjectIdSerializer extends StdSerializer<ObjectId> {
- *     public ObjectIdSerializer() {
- *         super(ObjectId.class);
- *     }
- *
- *     @Override
- *     public void serialize(ObjectId value, JsonGenerator jsonGenerator, SerializerProvider provider) throws IOException {
- *         jsonGenerator.writeString(value.toString());
- *     }
- * }
- *
- * public class ObjectIdDeserializer extends StdDeserializer<ObjectId> {
- *     public ObjectIdDeserializer() {
- *         super(ObjectId.class);
- *     }
- *
- *     @Override
- *     public ObjectId deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException {
- *         return ObjectId.massageToObjectId(jsonParser.getValueAsString());
- *     }
- * }
- *
- * public class ObjectIdModule extends SimpleModule {
- *     public ObjectIdModule() {
- *         // first deserializers
- *         addDeserializer(ObjectId.class, new ObjectIdDeserializer());
- *
- *         // then serializers:
- *         addSerializer(ObjectId.class, new ObjectIdSerializer());
- *     }
- * }
- *
- * @Provides(type = Provides.Type.SET)
- * Module objectIdModule() {
- *     return new ObjectIdModule();
- * }
- * 
- */ -@dagger.Module(injects = Feign.class, addsTo = Feign.Defaults.class) -public final class JacksonModule { - @Provides Encoder encoder(ObjectMapper mapper) { - return new JacksonEncoder(mapper); - } - - @Provides Decoder decoder(ObjectMapper mapper) { - return new JacksonDecoder(mapper); - } - - @Provides @Singleton ObjectMapper mapper(Set modules) { - return new ObjectMapper() - .setSerializationInclusion(JsonInclude.Include.NON_NULL) - .configure(SerializationFeature.INDENT_OUTPUT, true) - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .registerModules(modules); - } - - @Provides(type = Provides.Type.SET_VALUES) Set noDefaultModules() { - return Collections.emptySet(); - } -} diff --git a/jackson/src/test/java/feign/jackson/JacksonModuleTest.java b/jackson/src/test/java/feign/jackson/JacksonCodecTest.java similarity index 63% rename from jackson/src/test/java/feign/jackson/JacksonModuleTest.java rename to jackson/src/test/java/feign/jackson/JacksonCodecTest.java index 698feb324d..f59a7bfa37 100644 --- a/jackson/src/test/java/feign/jackson/JacksonModuleTest.java +++ b/jackson/src/test/java/feign/jackson/JacksonCodecTest.java @@ -4,15 +4,11 @@ import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import com.fasterxml.jackson.databind.module.SimpleModule; -import dagger.Module; -import dagger.ObjectGraph; -import dagger.Provides; import feign.RequestTemplate; import feign.Response; -import feign.codec.Decoder; -import feign.codec.Encoder; import java.io.IOException; import java.util.Arrays; import java.util.Collection; @@ -21,7 +17,6 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; -import javax.inject.Inject; import org.junit.Test; import static feign.Util.UTF_8; @@ -29,38 +24,14 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; -public class JacksonModuleTest { - @Module(includes = JacksonModule.class, injects = EncoderAndDecoderBindings.class) - static class EncoderAndDecoderBindings { - @Inject - Encoder encoder; - @Inject - Decoder decoder; - } - - @Test - public void providesEncoderDecoder() throws Exception { - EncoderAndDecoderBindings bindings = new EncoderAndDecoderBindings(); - ObjectGraph.create(bindings).inject(bindings); - - assertEquals(JacksonEncoder.class, bindings.encoder.getClass()); - assertEquals(JacksonDecoder.class, bindings.decoder.getClass()); - } - - @Module(includes = JacksonModule.class, injects = EncoderBindings.class) - static class EncoderBindings { - @Inject Encoder encoder; - } +public class JacksonCodecTest { @Test public void encodesMapObjectNumericalValuesAsInteger() throws Exception { - EncoderBindings bindings = new EncoderBindings(); - ObjectGraph.create(bindings).inject(bindings); - Map map = new LinkedHashMap(); map.put("foo", 1); RequestTemplate template = new RequestTemplate(); - bindings.encoder.encode(map, template); + new JacksonEncoder().encode(map, template); assertThat(template).hasBody(""// + "{\n" // @@ -69,15 +40,12 @@ static class EncoderBindings { } @Test public void encodesFormParams() throws Exception { - EncoderBindings bindings = new EncoderBindings(); - ObjectGraph.create(bindings).inject(bindings); - Map form = new LinkedHashMap(); form.put("foo", 1); form.put("bar", Arrays.asList(2, 3)); RequestTemplate template = new RequestTemplate(); - bindings.encoder.encode(form, template); + new JacksonEncoder().encode(form, template); assertThat(template).hasBody(""// + "{\n" // @@ -105,31 +73,20 @@ static class Zone extends LinkedHashMap { private static final long serialVersionUID = 1L; } - @Module(includes = JacksonModule.class, injects = DecoderBindings.class) - static class DecoderBindings { - @Inject Decoder decoder; - } - @Test public void decodes() throws Exception { - DecoderBindings bindings = new DecoderBindings(); - ObjectGraph.create(bindings).inject(bindings); - List zones = new LinkedList(); zones.add(new Zone("denominator.io.")); zones.add(new Zone("denominator.io.", "ABCD")); Response response = Response.create(200, "OK", Collections.>emptyMap(), zonesJson, UTF_8); - assertEquals(zones, bindings.decoder.decode(response, new TypeReference>() { + assertEquals(zones, new JacksonDecoder().decode(response, new TypeReference>() { }.getType())); } @Test public void nullBodyDecodesToNull() throws Exception { - DecoderBindings bindings = new DecoderBindings(); - ObjectGraph.create(bindings).inject(bindings); - Response response = Response.create(204, "OK", Collections.>emptyMap(), (byte[]) null); - assertNull(bindings.decoder.decode(response, String.class)); + assertNull(new JacksonDecoder().decode(response, String.class)); } private String zonesJson = ""// @@ -169,19 +126,8 @@ public ZoneModule() { } } - @Module(includes = JacksonModule.class, injects = CustomJacksonModule.class) - static class CustomJacksonModule { - @Inject Decoder decoder; - - @Provides(type = Provides.Type.SET) - com.fasterxml.jackson.databind.Module upperZone() { - return new ZoneModule(); - } - } - @Test public void customDecoder() throws Exception { - CustomJacksonModule bindings = new CustomJacksonModule(); - ObjectGraph.create(bindings).inject(bindings); + JacksonDecoder decoder = new JacksonDecoder(Arrays.asList(new ZoneModule())); List zones = new LinkedList(); zones.add(new Zone("DENOMINATOR.IO.")); @@ -189,7 +135,7 @@ com.fasterxml.jackson.databind.Module upperZone() { Response response = Response.create(200, "OK", Collections.>emptyMap(), zonesJson, UTF_8); - assertEquals(zones, bindings.decoder.decode(response, new TypeReference>() { + assertEquals(zones, decoder.decode(response, new TypeReference>() { }.getType())); } } diff --git a/jackson/src/test/java/feign/jackson/examples/GitHubExample.java b/jackson/src/test/java/feign/jackson/examples/GitHubExample.java index 73bacef4c3..5ec2c2e975 100644 --- a/jackson/src/test/java/feign/jackson/examples/GitHubExample.java +++ b/jackson/src/test/java/feign/jackson/examples/GitHubExample.java @@ -4,7 +4,6 @@ import feign.Param; import feign.RequestLine; import feign.jackson.JacksonDecoder; - import java.util.List; /** @@ -29,8 +28,11 @@ void setContributions(int contributions) { } } - public static void main(String... args) throws InterruptedException { - GitHub github = Feign.builder().decoder(new JacksonDecoder()).target(GitHub.class, "https://api.github.com"); + public static void main(String... args) { + GitHub github = Feign.builder() + .decoder(new JacksonDecoder()) + .target(GitHub.class, "https://api.github.com"); + System.out.println("Let's fetch and print a list of the contributors to this library."); List contributors = github.contributors("netflix", "feign"); for (Contributor contributor : contributors) { diff --git a/jaxb/README.md b/jaxb/README.md index 46e1e2d7a4..2c658a3af2 100644 --- a/jaxb/README.md +++ b/jaxb/README.md @@ -16,11 +16,3 @@ Response response = Feign.builder() .decoder(new JAXBDecoder(jaxbFactory)) .target(Response.class, "https://apihost"); ``` - -Alternatively, you can add the encoder and decoder to your Dagger object graph using the provided JAXBModule like so: - -```java -JAXBContextFactory jaxbFactory = new JAXBContextFactory.Builder().build(); - -Response response = Feign.create(Response.class, "https://apihost", new JAXBModule(jaxbFactory)); -``` \ No newline at end of file diff --git a/jaxb/src/main/java/feign/jaxb/JAXBDecoder.java b/jaxb/src/main/java/feign/jaxb/JAXBDecoder.java index b119463f28..2cbc3cdb09 100644 --- a/jaxb/src/main/java/feign/jaxb/JAXBDecoder.java +++ b/jaxb/src/main/java/feign/jaxb/JAXBDecoder.java @@ -19,12 +19,10 @@ import feign.Response; import feign.codec.DecodeException; import feign.codec.Decoder; - -import javax.inject.Inject; -import javax.xml.bind.JAXBException; -import javax.xml.bind.Unmarshaller; import java.io.IOException; import java.lang.reflect.Type; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Unmarshaller; /** * Decodes responses using JAXB. @@ -49,7 +47,6 @@ public class JAXBDecoder implements Decoder { private final JAXBContextFactory jaxbContextFactory; - @Inject public JAXBDecoder(JAXBContextFactory jaxbContextFactory) { this.jaxbContextFactory = jaxbContextFactory; } diff --git a/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java b/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java index acbf0ca34f..4b7801cb97 100644 --- a/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java +++ b/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java @@ -18,11 +18,9 @@ import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; - -import javax.inject.Inject; +import java.io.StringWriter; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; -import java.io.StringWriter; /** * Encodes requests using JAXB. @@ -47,7 +45,6 @@ public class JAXBEncoder implements Encoder { private final JAXBContextFactory jaxbContextFactory; - @Inject public JAXBEncoder(JAXBContextFactory jaxbContextFactory) { this.jaxbContextFactory = jaxbContextFactory; } diff --git a/jaxb/src/main/java/feign/jaxb/JAXBModule.java b/jaxb/src/main/java/feign/jaxb/JAXBModule.java deleted file mode 100644 index 94835dfef0..0000000000 --- a/jaxb/src/main/java/feign/jaxb/JAXBModule.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2014 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.jaxb; - -import dagger.Provides; -import feign.Feign; -import feign.codec.Decoder; -import feign.codec.Encoder; - -import javax.inject.Singleton; - -/** - * Provides an Encoder and Decoder for handling XML responses with JAXB annotated classes. - *

- *
- * Here is an example of configuring a custom JAXBContextFactory: - *

- *
- *    JAXBContextFactory jaxbFactory = new JAXBContextFactory.Builder()
- *               .withMarshallerJAXBEncoding("UTF-8")
- *               .withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd")
- *               .build();
- *
- *    Response response = Feign.create(Response.class, "http://apihost", new JAXBModule(jaxbFactory));
- * 
- *

- * The JAXBContextFactory should be reused across requests as it caches the created JAXB contexts. - *

- */ -@dagger.Module(injects = Feign.class, addsTo = Feign.Defaults.class) -public final class JAXBModule { - private final JAXBContextFactory jaxbContextFactory; - - public JAXBModule() { - this.jaxbContextFactory = new JAXBContextFactory.Builder().build(); - } - - public JAXBModule(JAXBContextFactory jaxbContextFactory) { - this.jaxbContextFactory = jaxbContextFactory; - } - - @Provides Encoder encoder(JAXBEncoder jaxbEncoder) { - return jaxbEncoder; - } - - @Provides Decoder decoder(JAXBDecoder jaxbDecoder) { - return jaxbDecoder; - } - - @Provides @Singleton JAXBContextFactory jaxbContextFactory() { - return this.jaxbContextFactory; - } -} diff --git a/jaxb/src/test/java/feign/jaxb/JAXBModuleTest.java b/jaxb/src/test/java/feign/jaxb/JAXBCodecTest.java similarity index 73% rename from jaxb/src/test/java/feign/jaxb/JAXBModuleTest.java rename to jaxb/src/test/java/feign/jaxb/JAXBCodecTest.java index bc0ed745c0..30bb99ff7b 100644 --- a/jaxb/src/test/java/feign/jaxb/JAXBModuleTest.java +++ b/jaxb/src/test/java/feign/jaxb/JAXBCodecTest.java @@ -15,15 +15,11 @@ */ package feign.jaxb; -import dagger.Module; -import dagger.ObjectGraph; import feign.RequestTemplate; import feign.Response; -import feign.codec.Decoder; import feign.codec.Encoder; import java.util.Collection; import java.util.Collections; -import javax.inject.Inject; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; @@ -34,34 +30,7 @@ import static feign.assertj.FeignAssertions.assertThat; import static org.junit.Assert.assertEquals; -public class JAXBModuleTest { - @Module(includes = JAXBModule.class, injects = EncoderAndDecoderBindings.class) - static class EncoderAndDecoderBindings { - @Inject - Encoder encoder; - - @Inject - Decoder decoder; - } - - @Module(includes = JAXBModule.class, injects = EncoderBindings.class) - static class EncoderBindings { - @Inject Encoder encoder; - } - - @Module(includes = JAXBModule.class, injects = DecoderBindings.class) - static class DecoderBindings { - @Inject Decoder decoder; - } - - @Test - public void providesEncoderDecoder() throws Exception { - EncoderAndDecoderBindings bindings = new EncoderAndDecoderBindings(); - ObjectGraph.create(bindings).inject(bindings); - - assertEquals(JAXBEncoder.class, bindings.encoder.getClass()); - assertEquals(JAXBDecoder.class, bindings.decoder.getClass()); - } +public class JAXBCodecTest { @XmlRootElement @XmlAccessorType(XmlAccessType.FIELD) @@ -87,14 +56,11 @@ public int hashCode() { @Test public void encodesXml() throws Exception { - EncoderBindings bindings = new EncoderBindings(); - ObjectGraph.create(bindings).inject(bindings); - MockObject mock = new MockObject(); mock.value = "Test"; RequestTemplate template = new RequestTemplate(); - bindings.encoder.encode(mock, template); + new JAXBEncoder(new JAXBContextFactory.Builder().build()).encode(mock, template); assertThat(template).hasBody( "Test"); @@ -106,8 +72,7 @@ public void encodesXmlWithCustomJAXBEncoding() throws Exception { .withMarshallerJAXBEncoding("UTF-16") .build(); - JAXBModule jaxbModule = new JAXBModule(jaxbContextFactory); - Encoder encoder = jaxbModule.encoder(new JAXBEncoder(jaxbContextFactory)); + Encoder encoder = new JAXBEncoder(jaxbContextFactory); MockObject mock = new MockObject(); mock.value = "Test"; @@ -125,8 +90,7 @@ public void encodesXmlWithCustomJAXBSchemaLocation() throws Exception { .withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd") .build(); - JAXBModule jaxbModule = new JAXBModule(jaxbContextFactory); - Encoder encoder = jaxbModule.encoder(new JAXBEncoder(jaxbContextFactory)); + Encoder encoder = new JAXBEncoder(jaxbContextFactory); MockObject mock = new MockObject(); mock.value = "Test"; @@ -146,8 +110,7 @@ public void encodesXmlWithCustomJAXBNoNamespaceSchemaLocation() throws Exception .withMarshallerNoNamespaceSchemaLocation("http://apihost/schema.xsd") .build(); - JAXBModule jaxbModule = new JAXBModule(jaxbContextFactory); - Encoder encoder = jaxbModule.encoder(new JAXBEncoder(jaxbContextFactory)); + Encoder encoder = new JAXBEncoder(jaxbContextFactory); MockObject mock = new MockObject(); mock.value = "Test"; @@ -167,8 +130,7 @@ public void encodesXmlWithCustomJAXBFormattedOutput() { .withMarshallerFormattedOutput(true) .build(); - JAXBModule jaxbModule = new JAXBModule(jaxbContextFactory); - Encoder encoder = jaxbModule.encoder(new JAXBEncoder(jaxbContextFactory)); + Encoder encoder = new JAXBEncoder(jaxbContextFactory); MockObject mock = new MockObject(); mock.value = "Test"; @@ -187,9 +149,6 @@ public void encodesXmlWithCustomJAXBFormattedOutput() { @Test public void decodesXml() throws Exception { - DecoderBindings bindings = new DecoderBindings(); - ObjectGraph.create(bindings).inject(bindings); - MockObject mock = new MockObject(); mock.value = "Test"; @@ -199,6 +158,8 @@ public void decodesXml() throws Exception { Response response = Response.create(200, "OK", Collections.>emptyMap(), mockXml, UTF_8); - assertEquals(mock, bindings.decoder.decode(response, MockObject.class)); + JAXBDecoder decoder = new JAXBDecoder(new JAXBContextFactory.Builder().build()); + + assertEquals(mock, decoder.decode(response, MockObject.class)); } } diff --git a/jaxrs/src/main/java/feign/jaxrs/JAXRSContract.java b/jaxrs/src/main/java/feign/jaxrs/JAXRSContract.java new file mode 100644 index 0000000000..34d9526f30 --- /dev/null +++ b/jaxrs/src/main/java/feign/jaxrs/JAXRSContract.java @@ -0,0 +1,123 @@ +/* + * 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; + +import feign.Contract; +import feign.MethodMetadata; + +import javax.ws.rs.Consumes; +import javax.ws.rs.FormParam; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.HttpMethod; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Collection; + +import static feign.Util.checkState; +import static feign.Util.emptyToNull; + +/** + * Please refer to the + * Feign JAX-RS README. + */ +public final class JAXRSContract extends Contract.BaseContract { + static final String ACCEPT = "Accept"; + static final String CONTENT_TYPE = "Content-Type"; + + @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()); + if (!pathValue.startsWith("/")) { + pathValue = "/" + pathValue; + } + md.template().insert(0, pathValue); + } + return md; + } + + @Override + protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) { + Class annotationType = methodAnnotation.annotationType(); + HttpMethod http = annotationType.getAnnotation(HttpMethod.class); + if (http != null) { + checkState(data.template().method() == null, + "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 == Path.class) { + String pathValue = emptyToNull(Path.class.cast(methodAnnotation).value()); + checkState(pathValue != null, "Path.value() was empty on method %s", method.getName()); + String methodAnnotationValue = Path.class.cast(methodAnnotation).value(); + if (!methodAnnotationValue.startsWith("/") && !data.template().toString().endsWith("/")) { + methodAnnotationValue = "/" + methodAnnotationValue; + } + data.template().append(methodAnnotationValue); + } else if (annotationType == Produces.class) { + 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) { + 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); + } + } + + @Override + protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) { + boolean isHttpParam = false; + for (Annotation parameterAnnotation : annotations) { + Class 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 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 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; + } + } + return isHttpParam; + } +} diff --git a/jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java b/jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java deleted file mode 100644 index 1560058f3c..0000000000 --- a/jaxrs/src/main/java/feign/jaxrs/JAXRSModule.java +++ /dev/null @@ -1,133 +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; - -import dagger.Provides; -import feign.Body; -import feign.Contract; -import feign.MethodMetadata; - -import javax.ws.rs.Consumes; -import javax.ws.rs.FormParam; -import javax.ws.rs.HeaderParam; -import javax.ws.rs.HttpMethod; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; -import java.util.Collection; - -import static feign.Util.checkState; -import static feign.Util.emptyToNull; - -/** - * Please refer to the - * Feign JAX-RS README. - */ -@dagger.Module(library = true, overrides = true) -public final class JAXRSModule { - static final String ACCEPT = "Accept"; - static final String CONTENT_TYPE = "Content-Type"; - - @Provides Contract provideContract() { - return new JAXRSContract(); - } - - 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()); - if (!pathValue.startsWith("/")) { - pathValue = "/" + pathValue; - } - md.template().insert(0, pathValue); - } - return md; - } - - @Override - protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) { - Class annotationType = methodAnnotation.annotationType(); - HttpMethod http = annotationType.getAnnotation(HttpMethod.class); - if (http != null) { - checkState(data.template().method() == null, - "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 == Path.class) { - String pathValue = emptyToNull(Path.class.cast(methodAnnotation).value()); - checkState(pathValue != null, "Path.value() was empty on method %s", method.getName()); - String methodAnnotationValue = Path.class.cast(methodAnnotation).value(); - if (!methodAnnotationValue.startsWith("/") && !data.template().toString().endsWith("/")) { - methodAnnotationValue = "/" + methodAnnotationValue; - } - data.template().append(methodAnnotationValue); - } else if (annotationType == Produces.class) { - 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) { - 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); - } - } - - @Override - protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) { - boolean isHttpParam = false; - for (Annotation parameterAnnotation : annotations) { - Class 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 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 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; - } - } - return isHttpParam; - } - } -} diff --git a/jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java b/jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java index a88fcb5536..e3cb287292 100644 --- a/jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java +++ b/jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java @@ -44,14 +44,13 @@ import static org.assertj.core.data.MapEntry.entry; /** - * Tests interfaces defined per {@link feign.jaxrs.JAXRSModule.JAXRSContract} are interpreted into expected {@link feign + * Tests interfaces defined per {@link feign.jaxrs.JAXRSContract} are interpreted into expected {@link feign * .RequestTemplate template} * instances. */ public class JAXRSContractTest { @Rule public final ExpectedException thrown = ExpectedException.none(); - - JAXRSModule.JAXRSContract contract = new JAXRSModule.JAXRSContract(); + JAXRSContract contract = new JAXRSContract(); interface Methods { @POST void post(); diff --git a/jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java b/jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java index 5e99424460..2a21e4ddf8 100644 --- a/jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java +++ b/jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java @@ -15,17 +15,12 @@ */ package feign.jaxrs.examples; -import dagger.Module; -import dagger.Provides; import feign.Feign; -import feign.Logger; -import feign.gson.GsonModule; -import feign.jaxrs.JAXRSModule; - +import feign.jaxrs.JAXRSContract; +import java.util.List; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; -import java.util.List; /** * adapted from {@code com.example.retrofit.GitHubClient} @@ -43,7 +38,9 @@ static class Contributor { } public static void main(String... args) throws InterruptedException { - GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GitHubModule()); + GitHub github = Feign.builder() + .contract(new JAXRSContract()) + .target(GitHub.class, "https://api.github.com"); System.out.println("Let's fetch and print a list of the contributors to this library."); List contributors = github.contributors("netflix", "feign"); @@ -51,19 +48,4 @@ public static void main(String... args) throws InterruptedException { System.out.println(contributor.login + " (" + contributor.contributions + ")"); } } - - /** - * JAXRSModule tells us to process @GET etc annotations - */ - @Module(overrides = true, library = true, includes = {JAXRSModule.class, GsonModule.class}) - static class GitHubModule { - - @Provides Logger.Level loggingLevel() { - return Logger.Level.BASIC; - } - - @Provides Logger logger() { - return new Logger.ErrorLogger(); - } - } } diff --git a/ribbon/README.md b/ribbon/README.md index 02f72ef99e..4de2eba3f8 100644 --- a/ribbon/README.md +++ b/ribbon/README.md @@ -4,17 +4,17 @@ This module includes a feign `Target` and `Client` adapter to take advantage of ## Conventions This integration relies on the Feign `Target.url()` being encoded like `https://myAppProd` where `myAppProd` is the ribbon client or loadbalancer name and `myAppProd.ribbon.listOfServers` configuration is set. -### RibbonModule -Adding `RibbonModule` overrides URL resolution of Feign's client, adding smart routing and resiliency capabilities provided by Ribbon. +### RibbonClient +Adding `RibbonClient` overrides URL resolution of Feign's client, adding smart routing and resiliency capabilities provided by Ribbon. #### Usage instead of  ```java -MyService api = Feign.create(MyService.class, "https://myAppProd-1234567890.us-east-1.elb.amazonaws.com"); +MyService api = Feign.builder().target(MyService.class, "https://myAppProd-1234567890.us-east-1.elb.amazonaws.com"); ``` do ```java -MyService api = Feign.create(MyService.class, "https://myAppProd", new RibbonModule()); +MyService api = Feign.builder().client(new RibbonClient()).target(MyService.class, "https://myAppProd"); ``` ### LoadBalancingTarget Using or extending `LoadBalancingTarget` will enable dynamic url discovery via ribbon including incrementing server request counts. @@ -22,9 +22,9 @@ Using or extending `LoadBalancingTarget` will enable dynamic url discovery via r #### Usage instead of ```java -MyService api = Feign.create(MyService.class, "https://myAppProd-1234567890.us-east-1.elb.amazonaws.com"); +MyService api = Feign.builder().target(MyService.class, "https://myAppProd-1234567890.us-east-1.elb.amazonaws.com"); ``` do ```java -MyService api = Feign.create(LoadBalancingTarget.create(MyService.class, "https://myAppProd")); +MyService api = Feign.builder().target(LoadBalancingTarget.create(MyService.class, "https://myAppProd")); ``` diff --git a/ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java b/ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java index efa18e9243..d105702754 100644 --- a/ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java +++ b/ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java @@ -34,7 +34,7 @@ *
* Ex. *
- * MyService api = Feign.create(LoadBalancingTarget.create(MyService.class, "http://myAppProd"))
+ * MyService api = Feign.builder().target(LoadBalancingTarget.create(MyService.class, "http://myAppProd"))
  * 
* Where {@code myAppProd} is the ribbon loadbalancer name and {@code myAppProd.ribbon.listOfServers} configuration * is set. diff --git a/ribbon/src/main/java/feign/ribbon/RibbonClient.java b/ribbon/src/main/java/feign/ribbon/RibbonClient.java index 1535c24fdc..e9abdc7818 100644 --- a/ribbon/src/main/java/feign/ribbon/RibbonClient.java +++ b/ribbon/src/main/java/feign/ribbon/RibbonClient.java @@ -4,18 +4,11 @@ import com.netflix.client.ClientFactory; import com.netflix.client.config.IClientConfig; import com.netflix.loadbalancer.ILoadBalancer; - -import java.io.IOException; -import java.net.URI; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLSocketFactory; - import feign.Client; import feign.Request; import feign.Response; -import dagger.Lazy; +import java.io.IOException; +import java.net.URI; /** * RibbonClient can be used in Fiegn builder to activate smart routing and resiliency capabilities provided by Ribbon. @@ -31,18 +24,7 @@ public class RibbonClient implements Client { private final Client delegate; public RibbonClient() { - this.delegate = new Client.Default( - new Lazy() { - public SSLSocketFactory get() { - return (SSLSocketFactory)SSLSocketFactory.getDefault(); - } - }, - new Lazy() { - public HostnameVerifier get() { - return HttpsURLConnection.getDefaultHostnameVerifier(); - } - } - ); + this.delegate = new Client.Default(null, null); } public RibbonClient(Client delegate) { diff --git a/ribbon/src/main/java/feign/ribbon/RibbonModule.java b/ribbon/src/main/java/feign/ribbon/RibbonModule.java deleted file mode 100644 index 33ed6bc8e6..0000000000 --- a/ribbon/src/main/java/feign/ribbon/RibbonModule.java +++ /dev/null @@ -1,48 +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.ribbon; - -import dagger.Provides; -import feign.Client; -import javax.inject.Named; -import javax.inject.Singleton; - -/** - * Adding this module will override URL resolution of {@link feign.Client Feign's client}, - * adding smart routing and resiliency capabilities provided by Ribbon. - *
- * When using this, ensure the {@link feign.Target#url()} is set to as {@code http://clientName} - * or {@code https://clientName}. {@link com.netflix.client.config.IClientConfig#getClientName() clientName} - * will lookup the real url and port of your service dynamically. - *
- * Ex. - *
- * MyService api = Feign.create(MyService.class, "http://myAppProd", new RibbonModule());
- * 
- * Where {@code myAppProd} is the ribbon client name and {@code myAppProd.ribbon.listOfServers} configuration - * is set. - */ -@dagger.Module(overrides = true, library = true, complete = false) -public class RibbonModule { - - @Provides @Named("delegate") Client delegate(Client.Default delegate) { - return delegate; - } - - @Provides @Singleton Client httpClient(@Named("delegate") Client client) { - return new RibbonClient(client); - } -} diff --git a/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java b/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java index 346a2ff139..c1e05da961 100644 --- a/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java +++ b/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java @@ -18,24 +18,19 @@ import com.squareup.okhttp.mockwebserver.MockResponse; import com.squareup.okhttp.mockwebserver.SocketPolicy; import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule; -import dagger.Provides; import feign.Feign; import feign.Param; import feign.RequestLine; -import feign.codec.Decoder; -import feign.codec.Encoder; - import java.io.IOException; import java.net.URL; - -import static com.netflix.config.ConfigurationManager.getConfigInstance; -import static org.junit.Assert.assertEquals; - import org.junit.After; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; +import static com.netflix.config.ConfigurationManager.getConfigInstance; +import static org.junit.Assert.assertEquals; + public class RibbonClientTest { @Rule public final TestName testName = new TestName(); @Rule public final MockWebServerRule server1 = new MockWebServerRule(); @@ -44,17 +39,6 @@ public class RibbonClientTest { interface TestInterface { @RequestLine("POST /") void post(); @RequestLine("GET /?a={a}") void getWithQueryParameters(@Param("a") String a); - - @dagger.Module(injects = Feign.class, overrides = true, addsTo = Feign.Defaults.class) - static class Module { - @Provides Decoder defaultDecoder() { - return new Decoder.Default(); - } - - @Provides Encoder defaultEncoder() { - return new Encoder.Default(); - } - } } @Test public void loadBalancingDefaultPolicyRoundRobin() throws IOException, InterruptedException { @@ -63,8 +47,7 @@ static class Module { getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.getUrl("")) + "," + hostAndPort(server2.getUrl(""))); - TestInterface api = Feign.create(TestInterface.class, "http://" + client(), new TestInterface.Module(), - new RibbonModule()); + TestInterface api = Feign.builder().client(new RibbonClient()).target(TestInterface.class, "http://" + client()); api.post(); api.post(); @@ -81,9 +64,7 @@ static class Module { getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.getUrl(""))); - - TestInterface api = Feign.create(TestInterface.class, "http://" + client(), new TestInterface.Module(), - new RibbonModule()); + TestInterface api = Feign.builder().client(new RibbonClient()).target(TestInterface.class, "http://" + client()); api.post(); @@ -107,8 +88,7 @@ invalid characters (ex. space). getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.getUrl(""))); - TestInterface api = Feign.create(TestInterface.class, "http://" + client(), new TestInterface.Module(), - new RibbonModule()); + TestInterface api = Feign.builder().client(new RibbonClient()).target(TestInterface.class, "http://" + client()); api.getWithQueryParameters(queryStringValue); diff --git a/sax/src/main/java/feign/sax/SAXDecoder.java b/sax/src/main/java/feign/sax/SAXDecoder.java index 0afc817737..b038f85489 100644 --- a/sax/src/main/java/feign/sax/SAXDecoder.java +++ b/sax/src/main/java/feign/sax/SAXDecoder.java @@ -18,19 +18,17 @@ import feign.Response; import feign.codec.DecodeException; import feign.codec.Decoder; -import org.xml.sax.ContentHandler; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; -import org.xml.sax.XMLReader; -import org.xml.sax.helpers.XMLReaderFactory; - -import javax.inject.Provider; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Type; import java.util.LinkedHashMap; import java.util.Map; +import org.xml.sax.ContentHandler; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.XMLReaderFactory; import static feign.Util.checkNotNull; import static feign.Util.checkState; @@ -50,19 +48,6 @@ * .build()) * .target(MyApi.class, "http://api"); * - *

- *

Advanced example with Dagger

- *
- *
- * @Provides
- * Decoder saxDecoder(Provider<ContentHandlerForFoo> foo, //
- *         Provider<ContentHandlerForBar> bar) {
- *     return SAXDecoder.builder() //
- *             .registerContentHandler(Foo.class, foo) //
- *             .registerContentHandler(Bar.class, bar) //
- *             .build();
- * }
- * 
*/ public class SAXDecoder implements Decoder { @@ -70,10 +55,9 @@ public static Builder builder() { return new Builder(); } - // builder as dagger doesn't support wildcard bindings, map bindings, or set bindings of providers. public static class Builder { - private final Map>> handlerProviders = - new LinkedHashMap>>(); + private final Map> handlerFactories = + new LinkedHashMap>(); /** * Will call {@link Constructor#newInstance(Object...)} on {@code handlerClass} for each content stream. @@ -86,13 +70,13 @@ public static class Builder { */ public > Builder registerContentHandler(Class handlerClass) { Type type = resolveLastTypeParameter(checkNotNull(handlerClass, "handlerClass"), ContentHandlerWithResult.class); - return registerContentHandler(type, new NewInstanceProvider(handlerClass)); + return registerContentHandler(type, new NewInstanceContentHandlerWithResultFactory(handlerClass)); } - private static class NewInstanceProvider> implements Provider { - private final Constructor ctor; + private static class NewInstanceContentHandlerWithResultFactory implements ContentHandlerWithResult.Factory { + private final Constructor> ctor; - private NewInstanceProvider(Class clazz) { + private NewInstanceContentHandlerWithResultFactory(Class> clazz) { try { this.ctor = clazz.getDeclaredConstructor(); // allow private or package protected ctors @@ -102,7 +86,7 @@ private NewInstanceProvider(Class clazz) { } } - @Override public T get() { + @Override public ContentHandlerWithResult create() { try { return ctor.newInstance(); } catch (Exception e) { @@ -112,16 +96,16 @@ private NewInstanceProvider(Class clazz) { } /** - * Will call {@link Provider#get()} on {@code handler} for each content stream. + * Will call {@link ContentHandlerWithResult.Factory#create()} on {@code handler} for each content stream. * The {@code handler} is expected to have a generic parameter of {@code type}. */ - public Builder registerContentHandler(Type type, Provider> handler) { - this.handlerProviders.put(checkNotNull(type, "type"), checkNotNull(handler, "handler")); + public Builder registerContentHandler(Type type, ContentHandlerWithResult.Factory handler) { + this.handlerFactories.put(checkNotNull(type, "type"), checkNotNull(handler, "handler")); return this; } public SAXDecoder build() { - return new SAXDecoder(handlerProviders); + return new SAXDecoder(handlerFactories); } } @@ -129,16 +113,21 @@ public SAXDecoder build() { * Implementations are not intended to be shared across requests. */ public interface ContentHandlerWithResult extends ContentHandler { + + public interface Factory { + ContentHandlerWithResult create(); + } + /** * expected to be set following a call to {@link XMLReader#parse(InputSource)} */ T result(); } - private final Map>> handlerProviders; + private final Map> handlerFactories; - private SAXDecoder(Map>> handlerProviders) { - this.handlerProviders = handlerProviders; + private SAXDecoder(Map> handlerFactories) { + this.handlerFactories = handlerFactories; } @Override @@ -146,9 +135,9 @@ public Object decode(Response response, Type type) throws IOException, DecodeExc if (response.body() == null) { return null; } - Provider> handlerProvider = handlerProviders.get(type); - checkState(handlerProvider != null, "type %s not in configured handlers %s", type, handlerProviders.keySet()); - ContentHandlerWithResult handler = handlerProvider.get(); + ContentHandlerWithResult.Factory handlerFactory = handlerFactories.get(type); + checkState(handlerFactory != null, "type %s not in configured handlers %s", type, handlerFactories.keySet()); + ContentHandlerWithResult handler = handlerFactory.create(); try { XMLReader xmlReader = XMLReaderFactory.createXMLReader(); xmlReader.setFeature("http://xml.org/sax/features/namespaces", false); diff --git a/sax/src/test/java/feign/sax/SAXDecoderTest.java b/sax/src/test/java/feign/sax/SAXDecoderTest.java index f627464519..903eb60b3c 100644 --- a/sax/src/test/java/feign/sax/SAXDecoderTest.java +++ b/sax/src/test/java/feign/sax/SAXDecoderTest.java @@ -15,17 +15,12 @@ */ package feign.sax; -import dagger.ObjectGraph; -import dagger.Provides; import feign.Response; import feign.codec.Decoder; import java.io.IOException; import java.text.ParseException; import java.util.Collection; import java.util.Collections; -import javax.inject.Inject; -import javax.inject.Provider; -import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -35,26 +30,17 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; -// unbound wildcards are not currently injectable in dagger. -@SuppressWarnings("rawtypes") public class SAXDecoderTest { @Rule public final ExpectedException thrown = ExpectedException.none(); - @dagger.Module(injects = SAXDecoderTest.class) - static class Module { - @Provides Decoder saxDecoder(Provider networkStatus) { - return SAXDecoder.builder() // - .registerContentHandler(NetworkStatus.class, networkStatus) // - .registerContentHandler(NetworkStatusStringHandler.class) // - .build(); - } - } - - @Inject Decoder decoder; - - @Before public void inject() { - ObjectGraph.create(new Module()).inject(this); - } + Decoder decoder = SAXDecoder.builder() // + .registerContentHandler(NetworkStatus.class, new SAXDecoder.ContentHandlerWithResult.Factory() { + @Override public SAXDecoder.ContentHandlerWithResult create() { + return new NetworkStatusHandler(); + } + }) // + .registerContentHandler(NetworkStatusStringHandler.class) // + .build(); @Test public void parsesConfiguredTypes() throws ParseException, IOException { assertEquals(NetworkStatus.FAILED, decoder.decode(statusFailedResponse(), NetworkStatus.class)); @@ -87,8 +73,6 @@ static enum NetworkStatus { static class NetworkStatusStringHandler extends DefaultHandler implements SAXDecoder.ContentHandlerWithResult { - @Inject NetworkStatusStringHandler() { - } private StringBuilder currentText = new StringBuilder(); @@ -115,8 +99,6 @@ public void characters(char ch[], int start, int length) { static class NetworkStatusHandler extends DefaultHandler implements SAXDecoder.ContentHandlerWithResult { - @Inject NetworkStatusHandler() { - } private StringBuilder currentText = new StringBuilder(); From bceee32ea7717a525247a118a5acfd3f5a61e9c2 Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Mon, 26 Jan 2015 10:08:52 -0800 Subject: [PATCH 163/179] Removes unused imports in OkHttpClientTest --- okhttp/src/test/java/feign/okhttp/OkHttpClientTest.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/okhttp/src/test/java/feign/okhttp/OkHttpClientTest.java b/okhttp/src/test/java/feign/okhttp/OkHttpClientTest.java index 881c9ef2e1..1830c08ff4 100644 --- a/okhttp/src/test/java/feign/okhttp/OkHttpClientTest.java +++ b/okhttp/src/test/java/feign/okhttp/OkHttpClientTest.java @@ -16,16 +16,12 @@ package feign.okhttp; import com.squareup.okhttp.mockwebserver.MockResponse; -import com.squareup.okhttp.mockwebserver.SocketPolicy; import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule; -import dagger.Lazy; -import feign.Client; import feign.Feign; import feign.FeignException; import feign.Headers; import feign.RequestLine; import feign.Response; -import feign.Util; import java.io.ByteArrayInputStream; import java.io.IOException; import org.junit.Rule; @@ -35,8 +31,6 @@ import static feign.Util.UTF_8; import static feign.assertj.MockWebServerAssertions.assertThat; import static java.util.Arrays.asList; -import static org.assertj.core.data.MapEntry.entry; -import static org.hamcrest.core.Is.isA; import static org.junit.Assert.assertEquals; public class OkHttpClientTest { From 31915a6aa73c8d258b6320f90b35f0b583720c96 Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Mon, 26 Jan 2015 10:32:39 -0800 Subject: [PATCH 164/179] Removes outdated dagger reference --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 985cc64afc..478962fd27 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Feign makes writing java http clients easier -Feign is a java to http client binder inspired by [Dagger](https://github.com/square/dagger), [Retrofit](https://github.com/square/retrofit), [JAXRS-2.0](https://jax-rs-spec.java.net/nonav/2.0/apidocs/index.html), and [WebSocket](http://www.oracle.com/technetwork/articles/java/jsr356-1937161.html). Feign's first goal was reducing the complexity of binding [Denominator](https://github.com/Netflix/Denominator) uniformly to http apis regardless of [restfulness](http://www.slideshare.net/adrianfcole/99problems). +Feign is a java to http client binder inspired by [Retrofit](https://github.com/square/retrofit), [JAXRS-2.0](https://jax-rs-spec.java.net/nonav/2.0/apidocs/index.html), and [WebSocket](http://www.oracle.com/technetwork/articles/java/jsr356-1937161.html). Feign's first goal was reducing the complexity of binding [Denominator](https://github.com/Netflix/Denominator) uniformly to http apis regardless of [restfulness](http://www.slideshare.net/adrianfcole/99problems). ### Why Feign and not X? From f4342dce0642e3074ea4b110962e8e14660d14b9 Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Tue, 27 Jan 2015 10:00:12 -0800 Subject: [PATCH 165/179] Makes examples standalone and built from standard Gradle or Maven --- example-github/README.md | 10 ++ example-github/build.gradle | 22 ++--- example-github/pom.xml | 73 +++++++++++++++ .../feign/example/github/GitHubExample.java | 27 ++---- example-wikipedia/README.md | 10 ++ example-wikipedia/build.gradle | 22 ++--- example-wikipedia/pom.xml | 78 ++++++++++++++++ .../example/wikipedia/ResponseAdapter.java | 13 +-- .../example/wikipedia/WikipediaExample.java | 93 +++++++------------ settings.gradle | 2 +- 10 files changed, 241 insertions(+), 109 deletions(-) create mode 100644 example-github/README.md create mode 100644 example-github/pom.xml create mode 100644 example-wikipedia/README.md create mode 100644 example-wikipedia/pom.xml diff --git a/example-github/README.md b/example-github/README.md new file mode 100644 index 0000000000..6070b912b2 --- /dev/null +++ b/example-github/README.md @@ -0,0 +1,10 @@ +GitHub Example +=================== + +This is an example of a simple json client. + +=== Building example with Gradle +Install and run `gradle` to produce `build/wikipedia` + +=== Building example with Maven +Install and run `mvn` to produce `target/wikipedia` diff --git a/example-github/build.gradle b/example-github/build.gradle index 0ecc2871d7..c9558f24f1 100644 --- a/example-github/build.gradle +++ b/example-github/build.gradle @@ -1,15 +1,19 @@ -plugins { - id 'nebula.provided-base' version '2.0.1' -} +// NOTE: This module is intended to be a stand-alone example which does depend on nebula. +defaultTasks 'clean', 'fatJar' apply plugin: 'java' -sourceCompatibility = 1.6 +repositories { + mavenCentral() +} + +configurations { + compile +} dependencies { - compile 'com.netflix.feign:feign-core:5.3.0' - compile 'com.netflix.feign:feign-gson:5.3.0' - provided 'com.squareup.dagger:dagger-compiler:1.2.2' + compile 'com.netflix.feign:feign-core:7.1.0' + compile 'com.netflix.feign:feign-gson:7.1.0' } // create a self-contained jar that is executable @@ -49,7 +53,3 @@ task fatJar(dependsOn: classes, type: Jar) { srcFile.setExecutable(true, true) } } - -artifacts { - archives fatJar -} diff --git a/example-github/pom.xml b/example-github/pom.xml new file mode 100644 index 0000000000..778608ad71 --- /dev/null +++ b/example-github/pom.xml @@ -0,0 +1,73 @@ + + + 4.0.0 + + + org.sonatype.oss + oss-parent + 7 + + + com.netflix.feign + feign-example-github + jar + 7.1.0 + GitHub Example + + + + com.netflix.feign + feign-core + ${project.version} + + + com.netflix.feign + feign-gson + ${project.version} + + + + + package + + + org.apache.maven.plugins + maven-shade-plugin + 2.3 + + + package + + shade + + + + + feign.example.github.GitHubExample + + + false + + + + + + org.skife.maven + really-executable-jar-maven-plugin + 1.3.0 + + github + + + + package + + really-executable-jar + + + + + + + diff --git a/example-github/src/main/java/feign/example/github/GitHubExample.java b/example-github/src/main/java/feign/example/github/GitHubExample.java index 900bfc18b8..f1054f4cae 100644 --- a/example-github/src/main/java/feign/example/github/GitHubExample.java +++ b/example-github/src/main/java/feign/example/github/GitHubExample.java @@ -15,14 +15,11 @@ */ package feign.example.github; -import dagger.Module; -import dagger.Provides; import feign.Feign; import feign.Logger; +import feign.Param; import feign.RequestLine; -import feign.gson.GsonModule; - -import javax.inject.Named; +import feign.gson.GsonDecoder; import java.util.List; /** @@ -32,7 +29,7 @@ public class GitHubExample { interface GitHub { @RequestLine("GET /repos/{owner}/{repo}/contributors") - List contributors(@Named("owner") String owner, @Named("repo") String repo); + List contributors(@Param("owner") String owner, @Param("repo") String repo); } static class Contributor { @@ -41,7 +38,11 @@ static class Contributor { } public static void main(String... args) throws InterruptedException { - GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule(), new LogToStderr()); + GitHub github = Feign.builder() + .decoder(new GsonDecoder()) + .logger(new Logger.ErrorLogger()) + .logLevel(Logger.Level.BASIC) + .target(GitHub.class, "https://api.github.com"); System.out.println("Let's fetch and print a list of the contributors to this library."); List contributors = github.contributors("netflix", "feign"); @@ -49,16 +50,4 @@ public static void main(String... args) throws InterruptedException { System.out.println(contributor.login + " (" + contributor.contributions + ")"); } } - - @Module(overrides = true, library = true, includes = GsonModule.class) - static class LogToStderr { - - @Provides Logger.Level loggingLevel() { - return Logger.Level.BASIC; - } - - @Provides Logger logger() { - return new Logger.ErrorLogger(); - } - } } diff --git a/example-wikipedia/README.md b/example-wikipedia/README.md new file mode 100644 index 0000000000..e9094c6a6f --- /dev/null +++ b/example-wikipedia/README.md @@ -0,0 +1,10 @@ +Wikipedia Example +=================== + +This is an example of advanced json response parsing, including pagination. + +=== Building example with Gradle +Install and run `gradle` to produce `build/wikipedia` + +=== Building example with Maven +Install and run `mvn` to produce `target/wikipedia` diff --git a/example-wikipedia/build.gradle b/example-wikipedia/build.gradle index 05b31b48f0..e9489a488d 100644 --- a/example-wikipedia/build.gradle +++ b/example-wikipedia/build.gradle @@ -1,15 +1,19 @@ -plugins { - id 'nebula.provided-base' version '2.0.1' -} +// NOTE: This module is intended to be a stand-alone example which does depend on nebula. +defaultTasks 'clean', 'fatJar' apply plugin: 'java' -sourceCompatibility = 1.6 +repositories { + mavenCentral() +} + +configurations { + compile +} dependencies { - compile 'com.netflix.feign:feign-core:5.3.0' - compile 'com.netflix.feign:feign-gson:5.3.0' - provided 'com.squareup.dagger:dagger-compiler:1.2.2' + compile 'com.netflix.feign:feign-core:7.1.0' + compile 'com.netflix.feign:feign-gson:7.1.0' } // create a self-contained jar that is executable @@ -49,7 +53,3 @@ task fatJar(dependsOn: classes, type: Jar) { srcFile.setExecutable(true, true) } } - -artifacts { - archives fatJar -} diff --git a/example-wikipedia/pom.xml b/example-wikipedia/pom.xml new file mode 100644 index 0000000000..144378ca4a --- /dev/null +++ b/example-wikipedia/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + + + org.sonatype.oss + oss-parent + 7 + + + com.netflix.feign + feign-example-wikipedia + jar + 7.1.0 + Wikipedia Example + + + + com.netflix.feign + feign-core + ${project.version} + + + com.netflix.feign + feign-gson + ${project.version} + + + com.google.code.gson + gson + 2.2.4 + + + + + package + + + org.apache.maven.plugins + maven-shade-plugin + 2.3 + + + package + + shade + + + + + feign.example.wikipedia.WikipediaExample + + + false + + + + + + org.skife.maven + really-executable-jar-maven-plugin + 1.3.0 + + wikipedia + + + + package + + really-executable-jar + + + + + + + diff --git a/example-wikipedia/src/main/java/feign/example/wikipedia/ResponseAdapter.java b/example-wikipedia/src/main/java/feign/example/wikipedia/ResponseAdapter.java index e202cc109b..3c5d77c2e2 100644 --- a/example-wikipedia/src/main/java/feign/example/wikipedia/ResponseAdapter.java +++ b/example-wikipedia/src/main/java/feign/example/wikipedia/ResponseAdapter.java @@ -58,17 +58,11 @@ public WikipediaExample.Response read(JsonReader reader) throws IOException { } } reader.endObject(); - } else if ("query-continue".equals(nextName)) { + } else if ("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(); + if ("gsroffset".equals(reader.nextName())) { + pages.nextOffset = reader.nextLong(); } else { reader.skipValue(); } @@ -79,7 +73,6 @@ public WikipediaExample.Response read(JsonReader reader) throws IOException { } } reader.endObject(); - reader.close(); return pages; } diff --git a/example-wikipedia/src/main/java/feign/example/wikipedia/WikipediaExample.java b/example-wikipedia/src/main/java/feign/example/wikipedia/WikipediaExample.java index feb5712174..bdaad34ffa 100644 --- a/example-wikipedia/src/main/java/feign/example/wikipedia/WikipediaExample.java +++ b/example-wikipedia/src/main/java/feign/example/wikipedia/WikipediaExample.java @@ -15,30 +15,27 @@ */ package feign.example.wikipedia; -import com.google.gson.TypeAdapter; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; -import dagger.Module; -import dagger.Provides; import feign.Feign; import feign.Logger; +import feign.Param; import feign.RequestLine; -import feign.gson.GsonModule; - -import javax.inject.Named; +import feign.gson.GsonDecoder; import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; -import static dagger.Provides.Type.SET; - public class WikipediaExample { public static interface Wikipedia { - @RequestLine("GET /w/api.php?action=query&generator=search&prop=info&format=json&gsrsearch={search}") - Response search(@Named("search") String search); + @RequestLine("GET /w/api.php?action=query&continue=&generator=search&prop=info&format=json&gsrsearch={search}") + Response search(@Param("search") String search); - @RequestLine("GET /w/api.php?action=query&generator=search&prop=info&format=json&gsrsearch={search}&gsroffset={offset}") - Response resumeSearch(@Named("search") String search, @Named("offset") long offset); + @RequestLine("GET /w/api.php?action=query&continue=&generator=search&prop=info&format=json&gsrsearch={search}&gsroffset={offset}") + Response resumeSearch(@Param("search") String search, @Param("offset") long offset); } static class Page { @@ -47,15 +44,20 @@ static class Page { } public static class Response extends ArrayList { - /** - * when present, the position to resume the list. - */ + /** 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 WikipediaDecoder(), new LogToStderr()); + Gson gson = new GsonBuilder() + .registerTypeAdapter(new TypeToken>(){}.getType(), pagesAdapter) + .create(); + + Wikipedia wikipedia = Feign.builder() + .decoder(new GsonDecoder(gson)) + .logger(new Logger.ErrorLogger()) + .logLevel(Logger.Level.BASIC) + .target(Wikipedia.class, "http://en.wikipedia.org"); System.out.println("Let's search for PTAL!"); Iterator pages = lazySearch(wikipedia, "PTAL"); @@ -101,48 +103,25 @@ public void remove() { }; } - @Module(includes = GsonModule.class) - static class WikipediaDecoder { - - /** - * registers a gson {@link TypeAdapter} for {@code Response}. - */ - @Provides(type = SET) TypeAdapter pagesAdapter() { - return new ResponseAdapter() { - - @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; - } - }; - } - } - - @Module(overrides = true, library = true) - static class LogToStderr { + static ResponseAdapter pagesAdapter = new ResponseAdapter() { - @Provides Logger.Level loggingLevel() { - return Logger.Level.BASIC; + @Override protected String query() { + return "pages"; } - @Provides Logger logger() { - return new Logger.ErrorLogger(); + @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; } - } + }; } diff --git a/settings.gradle b/settings.gradle index 3b5fdad827..ccbde471c1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,5 @@ rootProject.name='feign' -include 'core', 'sax', 'gson', 'jackson', 'jaxb', 'jaxrs', 'okhttp', 'ribbon', 'slf4j', 'example-github', 'example-wikipedia' +include 'core', 'sax', 'gson', 'jackson', 'jaxb', 'jaxrs', 'okhttp', 'ribbon', 'slf4j' rootProject.children.each { childProject -> childProject.name = 'feign-' + childProject.name From 183a5a119ce2985cfe637113985b87e817ea9daf Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Sat, 31 Jan 2015 12:00:48 -0800 Subject: [PATCH 166/179] Makes body parameter type explicit Feign has `MethodMetadata.bodyType()`, but never passed it to encoders. Encoders that register type adapters need to do so based on the interface desired as opposed to the implementation class. This change breaks api compatibility for < 8.x, by requiring an additional arg on `Encoder.encode`. see https://github.com/square/retrofit/issues/713 --- CHANGELOG.md | 1 + core/src/main/java/feign/MethodMetadata.java | 1 + core/src/main/java/feign/ReflectiveFeign.java | 4 +- core/src/main/java/feign/Types.java | 5 + core/src/main/java/feign/codec/Encoder.java | 16 +- .../test/java/feign/DefaultContractTest.java | 8 + .../src/test/java/feign/FeignBuilderTest.java | 7 +- core/src/test/java/feign/FeignTest.java | 29 ++- .../feign/assertj/RequestTemplateAssert.java | 7 +- .../java/feign/codec/DefaultEncoderTest.java | 6 +- .../src/main/java/feign/gson/GsonEncoder.java | 5 +- .../test/java/feign/gson/GsonCodecTest.java | 32 ++- .../java/feign/jackson/JacksonEncoder.java | 7 +- .../java/feign/jackson/JacksonCodecTest.java | 56 ++++- .../java/feign/jaxb/JAXBContextFactory.java | 154 ++++++------- .../src/main/java/feign/jaxb/JAXBDecoder.java | 35 +-- .../src/main/java/feign/jaxb/JAXBEncoder.java | 32 +-- .../test/java/feign/jaxb/JAXBCodecTest.java | 218 ++++++++++-------- .../feign/jaxb/JAXBContextFactoryTest.java | 70 +++--- .../jaxb/examples/AWSSignatureVersion4.java | 217 +++++++++-------- .../java/feign/jaxb/examples/IAMExample.java | 82 +++---- .../feign/jaxb/examples/package-info.java | 3 +- .../java/feign/jaxrs/JAXRSContractTest.java | 10 +- 23 files changed, 561 insertions(+), 444 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa2bd3a946..61f3ae945b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ### Version 8.0 * Removes Dagger 1.x Dependency * Removes support for parameters annotated with `javax.inject.@Named`. Use `feign.@Param` instead. +* Makes body parameter type explicit. ### Version 7.1 * Introduces feign.@Param to annotate template parameters. Users must migrate from `javax.inject.@Named` to `feign.@Param` before updating to Feign 8.0. diff --git a/core/src/main/java/feign/MethodMetadata.java b/core/src/main/java/feign/MethodMetadata.java index bca3678cac..61bbc38a94 100644 --- a/core/src/main/java/feign/MethodMetadata.java +++ b/core/src/main/java/feign/MethodMetadata.java @@ -79,6 +79,7 @@ MethodMetadata bodyIndex(Integer bodyIndex) { return this; } + /** Type corresponding to {@link #bodyIndex()}. */ public Type bodyType() { return bodyType; } diff --git a/core/src/main/java/feign/ReflectiveFeign.java b/core/src/main/java/feign/ReflectiveFeign.java index 541bd5f69d..bbb1ac0d50 100644 --- a/core/src/main/java/feign/ReflectiveFeign.java +++ b/core/src/main/java/feign/ReflectiveFeign.java @@ -201,7 +201,7 @@ protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map}. */ + static final Type MAP_STRING_WILDCARD = new ParameterizedTypeImpl(null, Map.class, String.class, + new WildcardTypeImpl(new Type[] { Object.class }, new Type[] { })); + private static final Type[] EMPTY_TYPE_ARRAY = new Type[0]; private Types() { diff --git a/core/src/main/java/feign/codec/Encoder.java b/core/src/main/java/feign/codec/Encoder.java index f9ba93fe8b..b34c55242c 100644 --- a/core/src/main/java/feign/codec/Encoder.java +++ b/core/src/main/java/feign/codec/Encoder.java @@ -16,6 +16,7 @@ package feign.codec; import feign.RequestTemplate; +import java.lang.reflect.Type; import static java.lang.String.format; @@ -40,8 +41,8 @@ * } * * @Override - * public void encode(Object object, RequestTemplate template) { - * template.body(gson.toJson(object)); + * public void encode(Object object, Type bodyType, RequestTemplate template) { + * template.body(gson.toJson(object, bodyType)); * } * } * @@ -59,24 +60,25 @@ * */ public interface Encoder { + /** * Converts objects to an appropriate representation in the template. * * @param object what to encode as the request body. + * @param bodyType the type the object should be encoded as. {@code Map}, if form encoding. * @param template the request template to populate. * @throws EncodeException when encoding failed due to a checked exception. */ - void encode(Object object, RequestTemplate template) throws EncodeException; + void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException; /** * Default implementation of {@code Encoder}. */ class Default implements Encoder { - @Override - public void encode(Object object, RequestTemplate template) throws EncodeException { - if (object instanceof String) { + @Override public void encode(Object object, Type bodyType, RequestTemplate template) { + if (bodyType == String.class) { template.body(object.toString()); - } else if (object instanceof byte[]) { + } else if (bodyType == byte[].class) { template.body((byte[]) object, null); } else if (object != null) { throw new EncodeException(format("%s is not a type supported by this encoder.", object.getClass())); diff --git a/core/src/test/java/feign/DefaultContractTest.java b/core/src/test/java/feign/DefaultContractTest.java index 739d9884f8..12e7bba057 100644 --- a/core/src/test/java/feign/DefaultContractTest.java +++ b/core/src/test/java/feign/DefaultContractTest.java @@ -231,6 +231,14 @@ void login( ); } + /** Body type is only for the body param. */ + @Test public void formParamsDoesNotSetBodyType() throws Exception { + MethodMetadata md = contract.parseAndValidatateMetadata(FormParams.class.getDeclaredMethod("login", String.class, + String.class, String.class)); + + assertThat(md.bodyType()).isNull(); + } + interface HeaderParams { @RequestLine("POST /") @Headers({"Auth-Token: {Auth-Token}", "Auth-Token: Foo"}) diff --git a/core/src/test/java/feign/FeignBuilderTest.java b/core/src/test/java/feign/FeignBuilderTest.java index 5020e2b2b1..63d452ea03 100644 --- a/core/src/test/java/feign/FeignBuilderTest.java +++ b/core/src/test/java/feign/FeignBuilderTest.java @@ -18,10 +18,7 @@ import com.squareup.okhttp.mockwebserver.MockResponse; import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule; import feign.codec.Decoder; -import feign.codec.EncodeException; import feign.codec.Encoder; -import org.junit.Rule; - import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Type; @@ -29,6 +26,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Rule; import org.junit.Test; import static feign.assertj.MockWebServerAssertions.assertThat; @@ -63,8 +61,7 @@ interface TestInterface { String url = "http://localhost:" + server.getPort(); Encoder encoder = new Encoder() { - @Override - public void encode(Object object, RequestTemplate template) throws EncodeException { + @Override public void encode(Object object, Type bodyType, RequestTemplate template) { template.body(object.toString()); } }; diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index 939190a509..47e5e0914e 100644 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -16,11 +16,13 @@ package feign; import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; import com.squareup.okhttp.mockwebserver.MockResponse; import com.squareup.okhttp.mockwebserver.SocketPolicy; import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule; import feign.Target.HardCodedTarget; import feign.codec.Decoder; +import feign.codec.EncodeException; import feign.codec.Encoder; import feign.codec.ErrorDecoder; import feign.codec.StringDecoder; @@ -31,6 +33,7 @@ import java.util.Date; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -137,6 +140,26 @@ interface OtherTestInterface { .hasBody("[netflix, denominator, password]"); } + /** The type of a parameter value may not be the desired type to encode as. Prefer the interface type. */ + @Test public void bodyTypeCorrespondsWithParameterType() throws IOException, InterruptedException { + server.enqueue(new MockResponse().setBody("foo")); + + final AtomicReference encodedType = new AtomicReference(); + TestInterface api = new TestInterfaceBuilder() + .encoder(new Encoder.Default() { + @Override public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException { + encodedType.set(bodyType); + } + }) + .target("http://localhost:" + server.getPort()); + + api.body(Arrays.asList("netflix", "denominator", "password")); + + server.takeRequest(); + + assertThat(encodedType.get()).isEqualTo(new TypeToken>(){}.getType()); + } + @Test public void postGZIPEncodedBodyParam() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("foo")); @@ -348,7 +371,7 @@ static final class TestInterfaceBuilder { private final Feign.Builder delegate = new Feign.Builder() .decoder(new Decoder.Default()) .encoder(new Encoder() { - @Override public void encode(Object object, RequestTemplate template) { + @Override public void encode(Object object, Type bodyType, RequestTemplate template) { if (object instanceof Map) { template.body(new Gson().toJson(object)); } else { @@ -362,8 +385,8 @@ TestInterfaceBuilder requestInterceptor(RequestInterceptor requestInterceptor) { return this; } - TestInterfaceBuilder client(Client client) { - delegate.client(client); + TestInterfaceBuilder encoder(Encoder encoder) { + delegate.encoder(encoder); return this; } diff --git a/core/src/test/java/feign/assertj/RequestTemplateAssert.java b/core/src/test/java/feign/assertj/RequestTemplateAssert.java index 8283222063..b2145ae777 100644 --- a/core/src/test/java/feign/assertj/RequestTemplateAssert.java +++ b/core/src/test/java/feign/assertj/RequestTemplateAssert.java @@ -46,7 +46,12 @@ public RequestTemplateAssert hasUrl(String expected) { } public RequestTemplateAssert hasBody(String utf8Expected) { - return hasBody(utf8Expected.getBytes(UTF_8)); + isNotNull(); + if (actual.bodyTemplate() != null) { + failWithMessage("\nExpecting bodyTemplate to be null, but was:<%s>", actual.bodyTemplate()); + } + objects.assertEqual(info, new String(actual.body(), UTF_8), utf8Expected); + return this; } public RequestTemplateAssert hasBody(byte[] expected) { diff --git a/core/src/test/java/feign/codec/DefaultEncoderTest.java b/core/src/test/java/feign/codec/DefaultEncoderTest.java index 1b643aa940..71d3367491 100644 --- a/core/src/test/java/feign/codec/DefaultEncoderTest.java +++ b/core/src/test/java/feign/codec/DefaultEncoderTest.java @@ -34,14 +34,14 @@ public class DefaultEncoderTest { @Test public void testEncodesStrings() throws Exception { String content = "This is my content"; RequestTemplate template = new RequestTemplate(); - encoder.encode(content, template); + encoder.encode(content, String.class, template); assertEquals(content, new String(template.body(), UTF_8)); } @Test public void testEncodesByteArray() throws Exception { byte[] content = {12, 34, 56}; RequestTemplate template = new RequestTemplate(); - encoder.encode(content, template); + encoder.encode(content, byte[].class, template); assertTrue(Arrays.equals(content, template.body())); } @@ -49,6 +49,6 @@ public class DefaultEncoderTest { thrown.expect(EncodeException.class); thrown.expectMessage("is not a type supported by this encoder."); - encoder.encode(new Date(), new RequestTemplate()); + encoder.encode(new Date(), Date.class, new RequestTemplate()); } } diff --git a/gson/src/main/java/feign/gson/GsonEncoder.java b/gson/src/main/java/feign/gson/GsonEncoder.java index 57e1b54ca8..a01772b6f2 100644 --- a/gson/src/main/java/feign/gson/GsonEncoder.java +++ b/gson/src/main/java/feign/gson/GsonEncoder.java @@ -19,6 +19,7 @@ import com.google.gson.TypeAdapter; import feign.RequestTemplate; import feign.codec.Encoder; +import java.lang.reflect.Type; import java.util.Collections; public class GsonEncoder implements Encoder { @@ -36,7 +37,7 @@ public GsonEncoder(Gson gson) { this.gson = gson; } - @Override public void encode(Object object, RequestTemplate template) { - template.body(gson.toJson(object)); + @Override public void encode(Object object, Type bodyType, RequestTemplate template) { + template.body(gson.toJson(object, bodyType)); } } diff --git a/gson/src/test/java/feign/gson/GsonCodecTest.java b/gson/src/test/java/feign/gson/GsonCodecTest.java index dab2824db1..c3b1ba3b75 100644 --- a/gson/src/test/java/feign/gson/GsonCodecTest.java +++ b/gson/src/test/java/feign/gson/GsonCodecTest.java @@ -43,7 +43,7 @@ public class GsonCodecTest { map.put("foo", 1); RequestTemplate template = new RequestTemplate(); - new GsonEncoder().encode(map, template); + new GsonEncoder().encode(map, map.getClass(), template); assertThat(template).hasBody("" // + "{\n" // @@ -68,7 +68,7 @@ public class GsonCodecTest { form.put("bar", Arrays.asList(2, 3)); RequestTemplate template = new RequestTemplate(); - new GsonEncoder().encode(form, template); + new GsonEncoder().encode(form, new TypeToken>(){}.getType(), template); assertThat(template).hasBody("" // + "{\n" // @@ -129,7 +129,11 @@ static class Zone extends LinkedHashMap { final TypeAdapter upperZone = new TypeAdapter() { @Override public void write(JsonWriter out, Zone value) throws IOException { - throw new IllegalArgumentException(); + out.beginObject(); + for(Map.Entry entry : value.entrySet()) { + out.name(entry.getKey()).value(entry.getValue().toString().toUpperCase()); + } + out.endObject(); } @Override public Zone read(JsonReader in) throws IOException { @@ -155,4 +159,26 @@ static class Zone extends LinkedHashMap { assertEquals(zones, decoder.decode(response, new TypeToken>() { }.getType())); } + + @Test public void customEncoder() throws Exception { + GsonEncoder encoder = new GsonEncoder(Arrays.>asList(upperZone)); + + List zones = new LinkedList(); + zones.add(new Zone("denominator.io.")); + zones.add(new Zone("denominator.io.", "abcd")); + + RequestTemplate template = new RequestTemplate(); + encoder.encode(zones, new TypeToken>(){}.getType(), template); + + assertThat(template).hasBody("" // + + "[\n" // + + " {\n" // + + " \"name\": \"DENOMINATOR.IO.\"\n" // + + " },\n" // + + " {\n" // + + " \"name\": \"DENOMINATOR.IO.\",\n" // + + " \"id\": \"ABCD\"\n" // + + " }\n" // + + "]"); + } } diff --git a/jackson/src/main/java/feign/jackson/JacksonEncoder.java b/jackson/src/main/java/feign/jackson/JacksonEncoder.java index 2d0353f931..1b8db303fb 100644 --- a/jackson/src/main/java/feign/jackson/JacksonEncoder.java +++ b/jackson/src/main/java/feign/jackson/JacksonEncoder.java @@ -17,12 +17,14 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; +import java.lang.reflect.Type; import java.util.Collections; public class JacksonEncoder implements Encoder { @@ -43,9 +45,10 @@ public JacksonEncoder(ObjectMapper mapper) { this.mapper = mapper; } - @Override public void encode(Object object, RequestTemplate template) throws EncodeException { + @Override public void encode(Object object, Type bodyType, RequestTemplate template) { try { - template.body(mapper.writeValueAsString(object)); + JavaType javaType = mapper.getTypeFactory().constructType(bodyType); + template.body(mapper.writerWithType(javaType).writeValueAsString(object)); } catch (JsonProcessingException e) { throw new EncodeException(e.getMessage(), e); } diff --git a/jackson/src/test/java/feign/jackson/JacksonCodecTest.java b/jackson/src/test/java/feign/jackson/JacksonCodecTest.java index f59a7bfa37..3bcaaf06f8 100644 --- a/jackson/src/test/java/feign/jackson/JacksonCodecTest.java +++ b/jackson/src/test/java/feign/jackson/JacksonCodecTest.java @@ -1,12 +1,15 @@ package feign.jackson; +import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; import feign.RequestTemplate; import feign.Response; import java.io.IOException; @@ -31,7 +34,7 @@ public class JacksonCodecTest { map.put("foo", 1); RequestTemplate template = new RequestTemplate(); - new JacksonEncoder().encode(map, template); + new JacksonEncoder().encode(map, map.getClass(), template); assertThat(template).hasBody(""// + "{\n" // @@ -45,7 +48,8 @@ public class JacksonCodecTest { form.put("bar", Arrays.asList(2, 3)); RequestTemplate template = new RequestTemplate(); - new JacksonEncoder().encode(form, template); + new JacksonEncoder().encode(form, new TypeReference>() { + }.getType(), template); assertThat(template).hasBody(""// + "{\n" // @@ -120,14 +124,9 @@ public Zone deserialize(JsonParser jp, DeserializationContext ctxt) throws IOExc } } - static class ZoneModule extends SimpleModule { - public ZoneModule() { - addDeserializer(Zone.class, new ZoneDeserializer()); - } - } - @Test public void customDecoder() throws Exception { - JacksonDecoder decoder = new JacksonDecoder(Arrays.asList(new ZoneModule())); + JacksonDecoder decoder = new JacksonDecoder( + Arrays.asList(new SimpleModule().addDeserializer(Zone.class, new ZoneDeserializer()))); List zones = new LinkedList(); zones.add(new Zone("DENOMINATOR.IO.")); @@ -135,7 +134,42 @@ public ZoneModule() { Response response = Response.create(200, "OK", Collections.>emptyMap(), zonesJson, UTF_8); - assertEquals(zones, decoder.decode(response, new TypeReference>() { - }.getType())); + assertEquals(zones, decoder.decode(response, new TypeReference>(){}.getType())); + } + + static class ZoneSerializer extends StdSerializer { + public ZoneSerializer() { + super(Zone.class); + } + + @Override public void serialize(Zone value, JsonGenerator jgen, SerializerProvider provider) + throws IOException { + jgen.writeStartObject(); + for(Map.Entry entry : value.entrySet()) { + jgen.writeFieldName(entry.getKey()); + jgen.writeString(entry.getValue().toString().toUpperCase()); + } + jgen.writeEndObject(); + } + } + + @Test public void customEncoder() throws Exception { + JacksonEncoder encoder = new JacksonEncoder( + Arrays.asList(new SimpleModule().addSerializer(Zone.class, new ZoneSerializer()))); + + List zones = new LinkedList(); + zones.add(new Zone("denominator.io.")); + zones.add(new Zone("denominator.io.", "abcd")); + + RequestTemplate template = new RequestTemplate(); + encoder.encode(zones, new TypeReference>(){}.getType(), template); + + assertThat(template).hasBody("" // + + "[ {\n" + + " \"name\" : \"DENOMINATOR.IO.\"\n" + + "}, {\n" + + " \"name\" : \"DENOMINATOR.IO.\",\n" + + " \"id\" : \"ABCD\"\n" + + "} ]"); } } diff --git a/jaxb/src/main/java/feign/jaxb/JAXBContextFactory.java b/jaxb/src/main/java/feign/jaxb/JAXBContextFactory.java index 3929325972..b12ca5551e 100644 --- a/jaxb/src/main/java/feign/jaxb/JAXBContextFactory.java +++ b/jaxb/src/main/java/feign/jaxb/JAXBContextFactory.java @@ -29,100 +29,100 @@ * Creates and caches JAXB contexts as well as creates Marshallers and Unmarshallers for each context. */ public final class JAXBContextFactory { - private final ConcurrentHashMap jaxbContexts = new ConcurrentHashMap(64); - private final Map properties; + private final ConcurrentHashMap jaxbContexts = new ConcurrentHashMap(64); + private final Map properties; - private JAXBContextFactory(Map properties) { - this.properties = properties; + private JAXBContextFactory(Map properties) { + this.properties = properties; + } + + /** + * Creates a new {@link javax.xml.bind.Unmarshaller} that handles the supplied class. + */ + public Unmarshaller createUnmarshaller(Class clazz) throws JAXBException { + JAXBContext ctx = getContext(clazz); + return ctx.createUnmarshaller(); + } + + /** + * Creates a new {@link javax.xml.bind.Marshaller} that handles the supplied class. + */ + public Marshaller createMarshaller(Class clazz) throws JAXBException { + JAXBContext ctx = getContext(clazz); + Marshaller marshaller = ctx.createMarshaller(); + setMarshallerProperties(marshaller); + return marshaller; + } + + private void setMarshallerProperties(Marshaller marshaller) throws PropertyException { + Iterator keys = properties.keySet().iterator(); + + while (keys.hasNext()) { + String key = keys.next(); + marshaller.setProperty(key, properties.get(key)); + } + } + + private JAXBContext getContext(Class clazz) throws JAXBException { + JAXBContext jaxbContext = this.jaxbContexts.get(clazz); + if (jaxbContext == null) { + jaxbContext = JAXBContext.newInstance(clazz); + this.jaxbContexts.putIfAbsent(clazz, jaxbContext); } + return jaxbContext; + } + + /** + * Creates instances of {@link feign.jaxb.JAXBContextFactory} + */ + public static class Builder { + private final Map properties = new HashMap(5); /** - * Creates a new {@link javax.xml.bind.Unmarshaller} that handles the supplied class. + * Sets the jaxb.encoding property of any Marshaller created by this factory. */ - public Unmarshaller createUnmarshaller(Class clazz) throws JAXBException { - JAXBContext ctx = getContext(clazz); - return ctx.createUnmarshaller(); + public Builder withMarshallerJAXBEncoding(String value) { + properties.put(Marshaller.JAXB_ENCODING, value); + return this; } /** - * Creates a new {@link javax.xml.bind.Marshaller} that handles the supplied class. + * Sets the jaxb.schemaLocation property of any Marshaller created by this factory. */ - public Marshaller createMarshaller(Class clazz) throws JAXBException { - JAXBContext ctx = getContext(clazz); - Marshaller marshaller = ctx.createMarshaller(); - setMarshallerProperties(marshaller); - return marshaller; + public Builder withMarshallerSchemaLocation(String value) { + properties.put(Marshaller.JAXB_SCHEMA_LOCATION, value); + return this; } - private void setMarshallerProperties(Marshaller marshaller) throws PropertyException { - Iterator keys = properties.keySet().iterator(); - - while(keys.hasNext()) { - String key = keys.next(); - marshaller.setProperty(key, properties.get(key)); - } + /** + * Sets the jaxb.noNamespaceSchemaLocation property of any Marshaller created by this factory. + */ + public Builder withMarshallerNoNamespaceSchemaLocation(String value) { + properties.put(Marshaller.JAXB_NO_NAMESPACE_SCHEMA_LOCATION, value); + return this; } - private JAXBContext getContext(Class clazz) throws JAXBException { - JAXBContext jaxbContext = this.jaxbContexts.get(clazz); - if (jaxbContext == null) { - jaxbContext = JAXBContext.newInstance(clazz); - this.jaxbContexts.putIfAbsent(clazz, jaxbContext); - } - return jaxbContext; + /** + * Sets the jaxb.formatted.output property of any Marshaller created by this factory. + */ + public Builder withMarshallerFormattedOutput(Boolean value) { + properties.put(Marshaller.JAXB_FORMATTED_OUTPUT, value); + return this; } /** - * Creates instances of {@link feign.jaxb.JAXBContextFactory} + * Sets the jaxb.fragment property of any Marshaller created by this factory. */ - public static class Builder { - private final Map properties = new HashMap(5); - - /** - * Sets the jaxb.encoding property of any Marshaller created by this factory. - */ - public Builder withMarshallerJAXBEncoding(String value) { - properties.put(Marshaller.JAXB_ENCODING, value); - return this; - } - - /** - * Sets the jaxb.schemaLocation property of any Marshaller created by this factory. - */ - public Builder withMarshallerSchemaLocation(String value) { - properties.put(Marshaller.JAXB_SCHEMA_LOCATION, value); - return this; - } - - /** - * Sets the jaxb.noNamespaceSchemaLocation property of any Marshaller created by this factory. - */ - public Builder withMarshallerNoNamespaceSchemaLocation(String value) { - properties.put(Marshaller.JAXB_NO_NAMESPACE_SCHEMA_LOCATION, value); - return this; - } - - /** - * Sets the jaxb.formatted.output property of any Marshaller created by this factory. - */ - public Builder withMarshallerFormattedOutput(Boolean value) { - properties.put(Marshaller.JAXB_FORMATTED_OUTPUT, value); - return this; - } - - /** - * Sets the jaxb.fragment property of any Marshaller created by this factory. - */ - public Builder withMarshallerFragment(Boolean value) { - properties.put(Marshaller.JAXB_FRAGMENT, value); - return this; - } + public Builder withMarshallerFragment(Boolean value) { + properties.put(Marshaller.JAXB_FRAGMENT, value); + return this; + } - /** - * Creates a new {@link feign.jaxb.JAXBContextFactory} instance. - */ - public JAXBContextFactory build() { - return new JAXBContextFactory(properties); - } + /** + * Creates a new {@link feign.jaxb.JAXBContextFactory} instance. + */ + public JAXBContextFactory build() { + return new JAXBContextFactory(properties); } + } } diff --git a/jaxb/src/main/java/feign/jaxb/JAXBDecoder.java b/jaxb/src/main/java/feign/jaxb/JAXBDecoder.java index 2cbc3cdb09..51775f9f6c 100644 --- a/jaxb/src/main/java/feign/jaxb/JAXBDecoder.java +++ b/jaxb/src/main/java/feign/jaxb/JAXBDecoder.java @@ -15,7 +15,6 @@ */ package feign.jaxb; -import feign.FeignException; import feign.Response; import feign.codec.DecodeException; import feign.codec.Decoder; @@ -45,23 +44,25 @@ *

*/ public class JAXBDecoder implements Decoder { - private final JAXBContextFactory jaxbContextFactory; + private final JAXBContextFactory jaxbContextFactory; - public JAXBDecoder(JAXBContextFactory jaxbContextFactory) { - this.jaxbContextFactory = jaxbContextFactory; - } + public JAXBDecoder(JAXBContextFactory jaxbContextFactory) { + this.jaxbContextFactory = jaxbContextFactory; + } - @Override - public Object decode(Response response, Type type) throws IOException, FeignException { - try { - Unmarshaller unmarshaller = jaxbContextFactory.createUnmarshaller((Class) type); - return unmarshaller.unmarshal(response.body().asInputStream()); - } catch (JAXBException e) { - throw new DecodeException(e.toString(), e); - } finally { - if(response.body() != null) { - response.body().close(); - } - } + @Override public Object decode(Response response, Type type) throws IOException { + if (!(type instanceof Class)) { + throw new UnsupportedOperationException("JAXB only supports decoding raw types. Found " + type); + } + try { + Unmarshaller unmarshaller = jaxbContextFactory.createUnmarshaller((Class) type); + return unmarshaller.unmarshal(response.body().asInputStream()); + } catch (JAXBException e) { + throw new DecodeException(e.toString(), e); + } finally { + if (response.body() != null) { + response.body().close(); + } } + } } diff --git a/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java b/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java index 4b7801cb97..79c546ef89 100644 --- a/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java +++ b/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java @@ -19,6 +19,8 @@ import feign.codec.EncodeException; import feign.codec.Encoder; import java.io.StringWriter; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; @@ -43,21 +45,23 @@ *

*/ public class JAXBEncoder implements Encoder { - private final JAXBContextFactory jaxbContextFactory; + private final JAXBContextFactory jaxbContextFactory; - public JAXBEncoder(JAXBContextFactory jaxbContextFactory) { - this.jaxbContextFactory = jaxbContextFactory; - } + public JAXBEncoder(JAXBContextFactory jaxbContextFactory) { + this.jaxbContextFactory = jaxbContextFactory; + } - @Override - public void encode(Object object, RequestTemplate template) throws EncodeException { - try { - Marshaller marshaller = jaxbContextFactory.createMarshaller(object.getClass()); - StringWriter stringWriter = new StringWriter(); - marshaller.marshal(object, stringWriter); - template.body(stringWriter.toString()); - } catch (JAXBException e) { - throw new EncodeException(e.toString(), e); - } + @Override public void encode(Object object, Type bodyType, RequestTemplate template) { + if (!(bodyType instanceof Class)) { + throw new UnsupportedOperationException("JAXB only supports encoding raw types. Found " + bodyType); + } + try { + Marshaller marshaller = jaxbContextFactory.createMarshaller((Class) bodyType); + StringWriter stringWriter = new StringWriter(); + marshaller.marshal(object, stringWriter); + template.body(stringWriter.toString()); + } catch (JAXBException e) { + throw new EncodeException(e.toString(), e); } + } } diff --git a/jaxb/src/test/java/feign/jaxb/JAXBCodecTest.java b/jaxb/src/test/java/feign/jaxb/JAXBCodecTest.java index 30bb99ff7b..051e644faf 100644 --- a/jaxb/src/test/java/feign/jaxb/JAXBCodecTest.java +++ b/jaxb/src/test/java/feign/jaxb/JAXBCodecTest.java @@ -18,148 +18,170 @@ import feign.RequestTemplate; import feign.Response; import feign.codec.Encoder; +import java.lang.reflect.Type; import java.util.Collection; import java.util.Collections; +import java.util.Map; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import static feign.Util.UTF_8; import static feign.assertj.FeignAssertions.assertThat; import static org.junit.Assert.assertEquals; public class JAXBCodecTest { + @Rule public final ExpectedException thrown = ExpectedException.none(); - @XmlRootElement - @XmlAccessorType(XmlAccessType.FIELD) - static class MockObject { - - @XmlElement - private String value; - - @Override - public boolean equals(Object obj) { - if (obj instanceof MockObject) { - MockObject other = (MockObject) obj; - return value.equals(other.value); - } - return false; - } - - @Override - public int hashCode() { - return value != null ? value.hashCode() : 0; - } - } + @XmlRootElement @XmlAccessorType(XmlAccessType.FIELD) static class MockObject { - @Test - public void encodesXml() throws Exception { - MockObject mock = new MockObject(); - mock.value = "Test"; + @XmlElement private String value; - RequestTemplate template = new RequestTemplate(); - new JAXBEncoder(new JAXBContextFactory.Builder().build()).encode(mock, template); + @Override public boolean equals(Object obj) { + if (obj instanceof MockObject) { + MockObject other = (MockObject) obj; + return value.equals(other.value); + } + return false; + } - assertThat(template).hasBody( - "Test"); + @Override public int hashCode() { + return value != null ? value.hashCode() : 0; } + } - @Test - public void encodesXmlWithCustomJAXBEncoding() throws Exception { - JAXBContextFactory jaxbContextFactory = new JAXBContextFactory.Builder() - .withMarshallerJAXBEncoding("UTF-16") - .build(); + @Test public void encodesXml() throws Exception { + MockObject mock = new MockObject(); + mock.value = "Test"; - Encoder encoder = new JAXBEncoder(jaxbContextFactory); + RequestTemplate template = new RequestTemplate(); + new JAXBEncoder(new JAXBContextFactory.Builder().build()).encode(mock, MockObject.class, template); - MockObject mock = new MockObject(); - mock.value = "Test"; + assertThat(template).hasBody( + "Test"); + } - RequestTemplate template = new RequestTemplate(); - encoder.encode(mock, template); + @Test public void doesntEncodeParameterizedTypes() throws Exception { + thrown.expect(UnsupportedOperationException.class); + thrown.expectMessage("JAXB only supports encoding raw types. Found java.util.Map"); - assertThat(template).hasBody("Test"); + class ParameterizedHolder { + Map field; } + Type parameterized = ParameterizedHolder.class.getDeclaredField("field").getGenericType(); - @Test - public void encodesXmlWithCustomJAXBSchemaLocation() throws Exception { - JAXBContextFactory jaxbContextFactory = new JAXBContextFactory.Builder() - .withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd") - .build(); + RequestTemplate template = new RequestTemplate(); + new JAXBEncoder(new JAXBContextFactory.Builder().build()).encode(Collections.emptyMap(), parameterized, template); + } - Encoder encoder = new JAXBEncoder(jaxbContextFactory); + @Test public void encodesXmlWithCustomJAXBEncoding() throws Exception { + JAXBContextFactory jaxbContextFactory = + new JAXBContextFactory.Builder().withMarshallerJAXBEncoding("UTF-16").build(); - MockObject mock = new MockObject(); - mock.value = "Test"; + Encoder encoder = new JAXBEncoder(jaxbContextFactory); - RequestTemplate template = new RequestTemplate(); - encoder.encode(mock, template); + MockObject mock = new MockObject(); + mock.value = "Test"; - assertThat(template).hasBody("" + - "Test"); - } + RequestTemplate template = new RequestTemplate(); + encoder.encode(mock, MockObject.class, template); - @Test - public void encodesXmlWithCustomJAXBNoNamespaceSchemaLocation() throws Exception { - JAXBContextFactory jaxbContextFactory = new JAXBContextFactory.Builder() - .withMarshallerNoNamespaceSchemaLocation("http://apihost/schema.xsd") - .build(); + assertThat(template).hasBody("Test"); + } - Encoder encoder = new JAXBEncoder(jaxbContextFactory); + @Test public void encodesXmlWithCustomJAXBSchemaLocation() throws Exception { + JAXBContextFactory jaxbContextFactory = + new JAXBContextFactory.Builder().withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd") + .build(); - MockObject mock = new MockObject(); - mock.value = "Test"; + Encoder encoder = new JAXBEncoder(jaxbContextFactory); - RequestTemplate template = new RequestTemplate(); - encoder.encode(mock, template); + MockObject mock = new MockObject(); + mock.value = "Test"; - assertThat(template).hasBody("" + - "Test"); - } + RequestTemplate template = new RequestTemplate(); + encoder.encode(mock, MockObject.class, template); - @Test - public void encodesXmlWithCustomJAXBFormattedOutput() { - JAXBContextFactory jaxbContextFactory = new JAXBContextFactory.Builder() - .withMarshallerFormattedOutput(true) - .build(); + assertThat(template).hasBody("" + + "Test"); + } - Encoder encoder = new JAXBEncoder(jaxbContextFactory); + @Test public void encodesXmlWithCustomJAXBNoNamespaceSchemaLocation() throws Exception { + JAXBContextFactory jaxbContextFactory = + new JAXBContextFactory.Builder().withMarshallerNoNamespaceSchemaLocation("http://apihost/schema.xsd").build(); - MockObject mock = new MockObject(); - mock.value = "Test"; + Encoder encoder = new JAXBEncoder(jaxbContextFactory); - RequestTemplate template = new RequestTemplate(); - encoder.encode(mock, template); + MockObject mock = new MockObject(); + mock.value = "Test"; - String NEWLINE = System.getProperty("line.separator"); + RequestTemplate template = new RequestTemplate(); + encoder.encode(mock, MockObject.class, template); - assertThat(template).hasBody(new StringBuilder() - .append("").append(NEWLINE) - .append("").append(NEWLINE) - .append(" Test").append(NEWLINE) - .append("").append(NEWLINE).toString()); - } + assertThat(template).hasBody("" + + "Test"); + } + + @Test public void encodesXmlWithCustomJAXBFormattedOutput() { + JAXBContextFactory jaxbContextFactory = + new JAXBContextFactory.Builder().withMarshallerFormattedOutput(true).build(); + + Encoder encoder = new JAXBEncoder(jaxbContextFactory); + + MockObject mock = new MockObject(); + mock.value = "Test"; - @Test - public void decodesXml() throws Exception { - MockObject mock = new MockObject(); - mock.value = "Test"; + RequestTemplate template = new RequestTemplate(); + encoder.encode(mock, MockObject.class, template); - String mockXml = "" + - "Test"; + String NEWLINE = System.getProperty("line.separator"); - Response response = - Response.create(200, "OK", Collections.>emptyMap(), mockXml, UTF_8); + assertThat(template).hasBody( + new StringBuilder().append("") + .append(NEWLINE) + .append("") + .append(NEWLINE) + .append(" Test") + .append(NEWLINE) + .append("") + .append(NEWLINE) + .toString()); + } - JAXBDecoder decoder = new JAXBDecoder(new JAXBContextFactory.Builder().build()); + @Test public void decodesXml() throws Exception { + MockObject mock = new MockObject(); + mock.value = "Test"; - assertEquals(mock, decoder.decode(response, MockObject.class)); + String mockXml = "" + + "Test"; + + Response response = Response.create(200, "OK", Collections.>emptyMap(), mockXml, UTF_8); + + JAXBDecoder decoder = new JAXBDecoder(new JAXBContextFactory.Builder().build()); + + assertEquals(mock, decoder.decode(response, MockObject.class)); + } + + @Test public void doesntDecodeParameterizedTypes() throws Exception { + thrown.expect(UnsupportedOperationException.class); + thrown.expectMessage("JAXB only supports decoding raw types. Found java.util.Map"); + + class ParameterizedHolder { + Map field; } + Type parameterized = ParameterizedHolder.class.getDeclaredField("field").getGenericType(); + + Response response = Response.create(200, "OK", Collections.>emptyMap(), "", UTF_8); + + new JAXBDecoder(new JAXBContextFactory.Builder().build()).decode(response, parameterized); + } } diff --git a/jaxb/src/test/java/feign/jaxb/JAXBContextFactoryTest.java b/jaxb/src/test/java/feign/jaxb/JAXBContextFactoryTest.java index b9ffbd308c..4410a666b2 100644 --- a/jaxb/src/test/java/feign/jaxb/JAXBContextFactoryTest.java +++ b/jaxb/src/test/java/feign/jaxb/JAXBContextFactoryTest.java @@ -22,55 +22,41 @@ import static org.junit.Assert.assertTrue; public class JAXBContextFactoryTest { - @Test - public void buildsMarshallerWithJAXBEncodingProperty() throws Exception { - JAXBContextFactory factory = new JAXBContextFactory.Builder() - .withMarshallerJAXBEncoding("UTF-16") - .build(); + @Test public void buildsMarshallerWithJAXBEncodingProperty() throws Exception { + JAXBContextFactory factory = new JAXBContextFactory.Builder().withMarshallerJAXBEncoding("UTF-16").build(); - Marshaller marshaller = factory.createMarshaller(Object.class); - assertEquals("UTF-16", marshaller.getProperty(Marshaller.JAXB_ENCODING)); - } + Marshaller marshaller = factory.createMarshaller(Object.class); + assertEquals("UTF-16", marshaller.getProperty(Marshaller.JAXB_ENCODING)); + } - @Test - public void buildsMarshallerWithSchemaLocationProperty() throws Exception { - JAXBContextFactory factory = new JAXBContextFactory.Builder() - .withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd") - .build(); + @Test public void buildsMarshallerWithSchemaLocationProperty() throws Exception { + JAXBContextFactory factory = + new JAXBContextFactory.Builder().withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd") + .build(); - Marshaller marshaller = factory.createMarshaller(Object.class); - assertEquals("http://apihost http://apihost/schema.xsd", - marshaller.getProperty(Marshaller.JAXB_SCHEMA_LOCATION)); - } + Marshaller marshaller = factory.createMarshaller(Object.class); + assertEquals("http://apihost http://apihost/schema.xsd", marshaller.getProperty(Marshaller.JAXB_SCHEMA_LOCATION)); + } - @Test - public void buildsMarshallerWithNoNamespaceSchemaLocationProperty() throws Exception { - JAXBContextFactory factory = new JAXBContextFactory.Builder() - .withMarshallerNoNamespaceSchemaLocation("http://apihost/schema.xsd") - .build(); + @Test public void buildsMarshallerWithNoNamespaceSchemaLocationProperty() throws Exception { + JAXBContextFactory factory = + new JAXBContextFactory.Builder().withMarshallerNoNamespaceSchemaLocation("http://apihost/schema.xsd").build(); - Marshaller marshaller = factory.createMarshaller(Object.class); - assertEquals("http://apihost/schema.xsd", - marshaller.getProperty(Marshaller.JAXB_NO_NAMESPACE_SCHEMA_LOCATION)); - } + Marshaller marshaller = factory.createMarshaller(Object.class); + assertEquals("http://apihost/schema.xsd", marshaller.getProperty(Marshaller.JAXB_NO_NAMESPACE_SCHEMA_LOCATION)); + } - @Test - public void buildsMarshallerWithFormattedOutputProperty() throws Exception { - JAXBContextFactory factory = new JAXBContextFactory.Builder() - .withMarshallerFormattedOutput(true) - .build(); + @Test public void buildsMarshallerWithFormattedOutputProperty() throws Exception { + JAXBContextFactory factory = new JAXBContextFactory.Builder().withMarshallerFormattedOutput(true).build(); - Marshaller marshaller = factory.createMarshaller(Object.class); - assertTrue((Boolean) marshaller.getProperty(Marshaller.JAXB_FORMATTED_OUTPUT)); - } + Marshaller marshaller = factory.createMarshaller(Object.class); + assertTrue((Boolean) marshaller.getProperty(Marshaller.JAXB_FORMATTED_OUTPUT)); + } - @Test - public void buildsMarshallerWithFragmentProperty() throws Exception { - JAXBContextFactory factory = new JAXBContextFactory.Builder() - .withMarshallerFragment(true) - .build(); + @Test public void buildsMarshallerWithFragmentProperty() throws Exception { + JAXBContextFactory factory = new JAXBContextFactory.Builder().withMarshallerFragment(true).build(); - Marshaller marshaller = factory.createMarshaller(Object.class); - assertTrue((Boolean) marshaller.getProperty(Marshaller.JAXB_FRAGMENT)); - } + Marshaller marshaller = factory.createMarshaller(Object.class); + assertTrue((Boolean) marshaller.getProperty(Marshaller.JAXB_FRAGMENT)); + } } diff --git a/jaxb/src/test/java/feign/jaxb/examples/AWSSignatureVersion4.java b/jaxb/src/test/java/feign/jaxb/examples/AWSSignatureVersion4.java index aaacbe71bc..683638fdc2 100644 --- a/jaxb/src/test/java/feign/jaxb/examples/AWSSignatureVersion4.java +++ b/jaxb/src/test/java/feign/jaxb/examples/AWSSignatureVersion4.java @@ -30,133 +30,132 @@ // http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html public class AWSSignatureVersion4 { - String region = "us-east-1"; - String service = "iam"; - String accessKey; - String secretKey; - - public AWSSignatureVersion4(String accessKey, String secretKey) { - this.accessKey = accessKey; - this.secretKey = secretKey; - } - - public Request apply(RequestTemplate input) { - if (!input.headers().isEmpty()) throw new UnsupportedOperationException("headers not supported"); - if (input.body() != null) throw new UnsupportedOperationException("body not supported"); - - String host = URI.create(input.url()).getHost(); - - String timestamp; - synchronized (iso8601) { - timestamp = iso8601.format(new Date()); - } - - String credentialScope = String.format("%s/%s/%s/%s", timestamp.substring(0, 8), region, service, "aws4_request"); - - input.query("X-Amz-Algorithm", "AWS4-HMAC-SHA256"); - input.query("X-Amz-Credential", accessKey + "/" + credentialScope); - input.query("X-Amz-Date", timestamp); - input.query("X-Amz-SignedHeaders", "host"); - input.header("Host", host); - - String canonicalString = canonicalString(input, host); - String toSign = toSign(timestamp, credentialScope, canonicalString); + String region = "us-east-1"; + String service = "iam"; + String accessKey; + String secretKey; - byte[] signatureKey = signatureKey(secretKey, timestamp); - String signature = hex(hmacSHA256(toSign, signatureKey)); + public AWSSignatureVersion4(String accessKey, String secretKey) { + this.accessKey = accessKey; + this.secretKey = secretKey; + } - input.query("X-Amz-Signature", signature); + public Request apply(RequestTemplate input) { + if (!input.headers().isEmpty()) throw new UnsupportedOperationException("headers not supported"); + if (input.body() != null) throw new UnsupportedOperationException("body not supported"); - return input.request(); - } + String host = URI.create(input.url()).getHost(); - byte[] signatureKey(String secretKey, String timestamp) { - byte[] kSecret = ("AWS4" + secretKey).getBytes(UTF_8); - byte[] kDate = hmacSHA256(timestamp.substring(0, 8), kSecret); - byte[] kRegion = hmacSHA256(region, kDate); - byte[] kService = hmacSHA256(service, kRegion); - byte[] kSigning = hmacSHA256("aws4_request", kService); - return kSigning; + String timestamp; + synchronized (iso8601) { + timestamp = iso8601.format(new Date()); } - static byte[] hmacSHA256(String data, byte[] key) { - try { - String algorithm = "HmacSHA256"; - Mac mac = Mac.getInstance(algorithm); - mac.init(new SecretKeySpec(key, algorithm)); - return mac.doFinal(data.getBytes(UTF_8)); - } catch (Exception e) { - throw new RuntimeException(e); - } + String credentialScope = String.format("%s/%s/%s/%s", timestamp.substring(0, 8), region, service, "aws4_request"); + + input.query("X-Amz-Algorithm", "AWS4-HMAC-SHA256"); + input.query("X-Amz-Credential", accessKey + "/" + credentialScope); + input.query("X-Amz-Date", timestamp); + input.query("X-Amz-SignedHeaders", "host"); + input.header("Host", host); + + String canonicalString = canonicalString(input, host); + String toSign = toSign(timestamp, credentialScope, canonicalString); + + byte[] signatureKey = signatureKey(secretKey, timestamp); + String signature = hex(hmacSHA256(toSign, signatureKey)); + + input.query("X-Amz-Signature", signature); + + return input.request(); + } + + byte[] signatureKey(String secretKey, String timestamp) { + byte[] kSecret = ("AWS4" + secretKey).getBytes(UTF_8); + byte[] kDate = hmacSHA256(timestamp.substring(0, 8), kSecret); + byte[] kRegion = hmacSHA256(region, kDate); + byte[] kService = hmacSHA256(service, kRegion); + byte[] kSigning = hmacSHA256("aws4_request", kService); + return kSigning; + } + + static byte[] hmacSHA256(String data, byte[] key) { + try { + String algorithm = "HmacSHA256"; + Mac mac = Mac.getInstance(algorithm); + mac.init(new SecretKeySpec(key, algorithm)); + return mac.doFinal(data.getBytes(UTF_8)); + } catch (Exception e) { + throw new RuntimeException(e); } + } - private static final String EMPTY_STRING_HASH = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + private static final String EMPTY_STRING_HASH = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; - private static String canonicalString(RequestTemplate input, String host) { - StringBuilder canonicalRequest = new StringBuilder(); - // HTTPRequestMethod + '\n' + - canonicalRequest.append(input.method()).append('\n'); + private static String canonicalString(RequestTemplate input, String host) { + StringBuilder canonicalRequest = new StringBuilder(); + // HTTPRequestMethod + '\n' + + canonicalRequest.append(input.method()).append('\n'); - // CanonicalURI + '\n' + - canonicalRequest.append(URI.create(input.url()).getPath()).append('\n'); + // CanonicalURI + '\n' + + canonicalRequest.append(URI.create(input.url()).getPath()).append('\n'); - // CanonicalQueryString + '\n' + - canonicalRequest.append(input.queryLine().substring(1)); - canonicalRequest.append('\n'); + // CanonicalQueryString + '\n' + + canonicalRequest.append(input.queryLine().substring(1)); + canonicalRequest.append('\n'); - // CanonicalHeaders + '\n' + - canonicalRequest.append("host:").append(host).append('\n'); + // CanonicalHeaders + '\n' + + canonicalRequest.append("host:").append(host).append('\n'); - canonicalRequest.append('\n'); + canonicalRequest.append('\n'); - // SignedHeaders + '\n' + - canonicalRequest.append("host").append('\n'); + // SignedHeaders + '\n' + + canonicalRequest.append("host").append('\n'); - // HexEncode(Hash(Payload)) - String bodyText = - input.charset() != null && input.body() != null ? new String(input.body(), input.charset()) : null; - if (bodyText != null) { - canonicalRequest.append(hex(sha256(bodyText))); - } else { - canonicalRequest.append(EMPTY_STRING_HASH); - } - return canonicalRequest.toString(); + // HexEncode(Hash(Payload)) + String bodyText = + input.charset() != null && input.body() != null ? new String(input.body(), input.charset()) : null; + if (bodyText != null) { + canonicalRequest.append(hex(sha256(bodyText))); + } else { + canonicalRequest.append(EMPTY_STRING_HASH); } - - private static String toSign(String timestamp, String credentialScope, String canonicalRequest) { - StringBuilder toSign = new StringBuilder(); - // Algorithm + '\n' + - toSign.append("AWS4-HMAC-SHA256").append('\n'); - // RequestDate + '\n' + - toSign.append(timestamp).append('\n'); - // CredentialScope + '\n' + - toSign.append(credentialScope).append('\n'); - // HexEncode(Hash(CanonicalRequest)) - toSign.append(hex(sha256(canonicalRequest))); - return toSign.toString(); + return canonicalRequest.toString(); + } + + private static String toSign(String timestamp, String credentialScope, String canonicalRequest) { + StringBuilder toSign = new StringBuilder(); + // Algorithm + '\n' + + toSign.append("AWS4-HMAC-SHA256").append('\n'); + // RequestDate + '\n' + + toSign.append(timestamp).append('\n'); + // CredentialScope + '\n' + + toSign.append(credentialScope).append('\n'); + // HexEncode(Hash(CanonicalRequest)) + toSign.append(hex(sha256(canonicalRequest))); + return toSign.toString(); + } + + private static String hex(byte[] data) { + StringBuilder result = new StringBuilder(data.length * 2); + for (byte b : data) { + result.append(String.format("%02x", b & 0xff)); } - - - private static String hex(byte[] data) { - StringBuilder result = new StringBuilder(data.length * 2); - for (byte b : data) { - result.append(String.format("%02x", b & 0xff)); - } - return result.toString(); + return result.toString(); + } + + static byte[] sha256(String data) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + return digest.digest(data.getBytes(UTF_8)); + } catch (Exception e) { + throw new RuntimeException(e); } + } - static byte[] sha256(String data) { - try { - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - return digest.digest(data.getBytes(UTF_8)); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - private static final SimpleDateFormat iso8601 = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"); + private static final SimpleDateFormat iso8601 = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"); - static { - iso8601.setTimeZone(TimeZone.getTimeZone("GMT")); - } + static { + iso8601.setTimeZone(TimeZone.getTimeZone("GMT")); + } } diff --git a/jaxb/src/test/java/feign/jaxb/examples/IAMExample.java b/jaxb/src/test/java/feign/jaxb/examples/IAMExample.java index cdf64245cf..e8443ffdf1 100644 --- a/jaxb/src/test/java/feign/jaxb/examples/IAMExample.java +++ b/jaxb/src/test/java/feign/jaxb/examples/IAMExample.java @@ -30,61 +30,53 @@ public class IAMExample { - interface IAM { - @RequestLine("GET /?Action=GetUser&Version=2010-05-08") GetUserResponse userResponse(); - } - - public static void main(String... args) { - IAM iam = Feign.builder() - .decoder(new JAXBDecoder(new JAXBContextFactory.Builder().build())) - .target(new IAMTarget(args[0], args[1])); - - GetUserResponse response = iam.userResponse(); - System.out.println("UserId: " + response.result.user.id); - } - - static class IAMTarget extends AWSSignatureVersion4 implements Target { + interface IAM { + @RequestLine("GET /?Action=GetUser&Version=2010-05-08") GetUserResponse userResponse(); + } - @Override public Class type() { - return IAM.class; - } + public static void main(String... args) { + IAM iam = Feign.builder() + .decoder(new JAXBDecoder(new JAXBContextFactory.Builder().build())) + .target(new IAMTarget(args[0], args[1])); - @Override public String name() { - return "iam"; - } + GetUserResponse response = iam.userResponse(); + System.out.println("UserId: " + response.result.user.id); + } - @Override public String url() { - return "https://iam.amazonaws.com"; - } + static class IAMTarget extends AWSSignatureVersion4 implements Target { - private IAMTarget(String accessKey, String secretKey) { - super(accessKey, secretKey); - } + @Override public Class type() { + return IAM.class; + } - @Override public Request apply(RequestTemplate in) { - in.insert(0, url()); - return super.apply(in); - } + @Override public String name() { + return "iam"; } - @XmlRootElement(name = "GetUserResponse", namespace = "https://iam.amazonaws.com/doc/2010-05-08/") - @XmlAccessorType(XmlAccessType.FIELD) - static class GetUserResponse { - @XmlElement(name = "GetUserResult") - private GetUserResult result; + @Override public String url() { + return "https://iam.amazonaws.com"; } - @XmlAccessorType(XmlAccessType.FIELD) - @XmlType(name = "GetUserResult") - static class GetUserResult { - @XmlElement(name = "User") - private User user; + private IAMTarget(String accessKey, String secretKey) { + super(accessKey, secretKey); } - @XmlAccessorType(XmlAccessType.FIELD) - @XmlType(name = "User") - static class User { - @XmlElement(name = "UserId") - private String id; + @Override public Request apply(RequestTemplate in) { + in.insert(0, url()); + return super.apply(in); } + } + + @XmlRootElement(name = "GetUserResponse", namespace = "https://iam.amazonaws.com/doc/2010-05-08/") + @XmlAccessorType(XmlAccessType.FIELD) static class GetUserResponse { + @XmlElement(name = "GetUserResult") private GetUserResult result; + } + + @XmlAccessorType(XmlAccessType.FIELD) @XmlType(name = "GetUserResult") static class GetUserResult { + @XmlElement(name = "User") private User user; + } + + @XmlAccessorType(XmlAccessType.FIELD) @XmlType(name = "User") static class User { + @XmlElement(name = "UserId") private String id; + } } diff --git a/jaxb/src/test/java/feign/jaxb/examples/package-info.java b/jaxb/src/test/java/feign/jaxb/examples/package-info.java index 0038947aa9..d52c85ad5e 100644 --- a/jaxb/src/test/java/feign/jaxb/examples/package-info.java +++ b/jaxb/src/test/java/feign/jaxb/examples/package-info.java @@ -13,5 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@javax.xml.bind.annotation.XmlSchema(namespace = "https://iam.amazonaws.com/doc/2010-05-08/", elementFormDefault = javax.xml.bind.annotation.XmlNsForm.QUALIFIED) -package feign.jaxb.examples; +@javax.xml.bind.annotation.XmlSchema(namespace = "https://iam.amazonaws.com/doc/2010-05-08/", elementFormDefault = javax.xml.bind.annotation.XmlNsForm.QUALIFIED) package feign.jaxb.examples; diff --git a/jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java b/jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java index e3cb287292..a4b286789f 100644 --- a/jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java +++ b/jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java @@ -44,7 +44,7 @@ import static org.assertj.core.data.MapEntry.entry; /** - * Tests interfaces defined per {@link feign.jaxrs.JAXRSContract} are interpreted into expected {@link feign + * Tests interfaces defined per {@link JAXRSContract} are interpreted into expected {@link feign * .RequestTemplate template} * instances. */ @@ -332,6 +332,14 @@ interface FormParams { ); } + /** Body type is only for the body param. */ + @Test public void formParamsDoesNotSetBodyType() throws Exception { + MethodMetadata md = contract.parseAndValidatateMetadata(FormParams.class.getDeclaredMethod("login", String.class, + String.class, String.class)); + + assertThat(md.bodyType()).isNull(); + } + @Test public void emptyFormParam() throws Exception { thrown.expect(IllegalStateException.class); thrown.expectMessage("FormParam.value() was empty on parameter 0"); From 76367fe03e1a60c5640ffef4db92e14d10a56c84 Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Sun, 1 Feb 2015 12:35:50 -0800 Subject: [PATCH 167/179] Adds EmptyTarget for interfaces who exclusively declare URI methods Supports cases when the base url isn't known until runtime. Closes #98 --- CHANGELOG.md | 3 + core/src/main/java/feign/Target.java | 60 +++++++++++++++++++ core/src/test/java/feign/EmptyTargetTest.java | 54 +++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 core/src/test/java/feign/EmptyTargetTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 61f3ae945b..2a6c134429 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ * Removes support for parameters annotated with `javax.inject.@Named`. Use `feign.@Param` instead. * Makes body parameter type explicit. +### Version 7.2 +* Adds EmptyTarget for interfaces who exclusively declare URI methods + ### Version 7.1 * Introduces feign.@Param to annotate template parameters. Users must migrate from `javax.inject.@Named` to `feign.@Param` before updating to Feign 8.0. * Supports custom expansion via `@Param(value = "name", expander = CustomExpander.class)` diff --git a/core/src/main/java/feign/Target.java b/core/src/main/java/feign/Target.java index 474ed29722..c161017b73 100644 --- a/core/src/main/java/feign/Target.java +++ b/core/src/main/java/feign/Target.java @@ -118,4 +118,64 @@ public HardCodedTarget(Class type, String name, String url) { return "HardCodedTarget(type=" + type.getSimpleName() + ", name=" + name + ", url=" + url + ")"; } } + + public static final class EmptyTarget implements Target { + private final Class type; + private final String name; + + EmptyTarget(Class type, String name) { + this.type = checkNotNull(type, "type"); + this.name = checkNotNull(emptyToNull(name), "name"); + } + + public static EmptyTarget create(Class type) { + return new EmptyTarget(type, "empty:" + type.getSimpleName()); + } + + public static EmptyTarget create(Class type, String name) { + return new EmptyTarget(type, name); + } + + @Override public Class type() { + return type; + } + + @Override public String name() { + return name; + } + + @Override public String url() { + throw new UnsupportedOperationException("Empty targets don't have URLs"); + } + + @Override public Request apply(RequestTemplate input) { + if (input.url().indexOf("http") != 0) { + throw new UnsupportedOperationException("Request with non-absolute URL not supported with empty target"); + } + return input.request(); + } + + @Override public boolean equals(Object obj) { + if (obj instanceof EmptyTarget) { + EmptyTarget other = (EmptyTarget) obj; + return type.equals(other.type) + && name.equals(other.name); + } + return false; + } + + @Override public int hashCode() { + int result = 17; + result = 31 * result + type.hashCode(); + result = 31 * result + name.hashCode(); + return result; + } + + @Override public String toString() { + if (name.equals("empty:" + type.getSimpleName())) { + return "EmptyTarget(type=" + type.getSimpleName() + ")"; + } + return "EmptyTarget(type=" + type.getSimpleName() + ", name=" + name + ")"; + } + } } diff --git a/core/src/test/java/feign/EmptyTargetTest.java b/core/src/test/java/feign/EmptyTargetTest.java new file mode 100644 index 0000000000..b90a71ba7a --- /dev/null +++ b/core/src/test/java/feign/EmptyTargetTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2015 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 feign.Target.EmptyTarget; +import java.net.URI; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static feign.assertj.FeignAssertions.assertThat; + +public class EmptyTargetTest { + @Rule public final ExpectedException thrown = ExpectedException.none(); + + interface UriInterface { + @RequestLine("GET /") Response get(URI endpoint); + } + + @Test public void whenNameNotSupplied() { + assertThat(EmptyTarget.create(UriInterface.class)) + .isEqualTo(EmptyTarget.create(UriInterface.class, "empty:UriInterface")); + } + + @Test public void toString_withoutName() { + assertThat(EmptyTarget.create(UriInterface.class).toString()) + .isEqualTo("EmptyTarget(type=UriInterface)"); + } + + @Test public void toString_withName() { + assertThat(EmptyTarget.create(UriInterface.class, "manager-access").toString()) + .isEqualTo("EmptyTarget(type=UriInterface, name=manager-access)"); + } + + @Test public void mustApplyToAbsoluteUrl() { + thrown.expect(UnsupportedOperationException.class); + thrown.expectMessage("Request with non-absolute URL not supported with empty target"); + + EmptyTarget.create(UriInterface.class).apply(new RequestTemplate().method("GET").append("/relative")); + } +} From 42fc4763f8bf203e0a39993c9d5e6b09f239ec87 Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Sun, 1 Feb 2015 13:48:40 -0800 Subject: [PATCH 168/179] corrects 7.2 changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a6c134429..b5ad693186 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ * Makes body parameter type explicit. ### Version 7.2 +* Adds `Feign.Builder.build()` +* Opens constructor for Gson and Jackson codecs which accepts type adapters * Adds EmptyTarget for interfaces who exclusively declare URI methods ### Version 7.1 From 207530d6c005591e0a65898c41c68e5808a8cf72 Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Sun, 1 Feb 2015 19:09:18 -0800 Subject: [PATCH 169/179] Reformats code according to Google Java Style Files had various formatting differences, as did pull requests. Rather than create our own style, this inherits and requires the well documented Google Java Style. --- CHANGELOG.md | 1 + CONTRIBUTING.md | 31 ++ core/src/main/java/feign/Body.java | 16 +- core/src/main/java/feign/Client.java | 37 +- core/src/main/java/feign/Contract.java | 67 ++- core/src/main/java/feign/Feign.java | 85 +-- core/src/main/java/feign/FeignException.java | 33 +- core/src/main/java/feign/Headers.java | 21 +- .../java/feign/InvocationHandlerFactory.java | 15 +- core/src/main/java/feign/Logger.java | 206 ++++---- core/src/main/java/feign/MethodMetadata.java | 20 +- core/src/main/java/feign/Param.java | 23 +- core/src/main/java/feign/ReflectiveFeign.java | 81 ++- core/src/main/java/feign/Request.java | 44 +- .../main/java/feign/RequestInterceptor.java | 42 +- core/src/main/java/feign/RequestLine.java | 28 +- core/src/main/java/feign/RequestTemplate.java | 405 +++++++------- core/src/main/java/feign/Response.java | 128 +++-- .../main/java/feign/RetryableException.java | 16 +- core/src/main/java/feign/Retryer.java | 35 +- .../java/feign/SynchronousMethodHandler.java | 70 +-- core/src/main/java/feign/Target.java | 86 +-- core/src/main/java/feign/Types.java | 148 ++++-- core/src/main/java/feign/Util.java | 37 +- core/src/main/java/feign/auth/Base64.java | 14 +- .../auth/BasicAuthRequestInterceptor.java | 26 +- .../java/feign/codec/DecodeException.java | 10 +- core/src/main/java/feign/codec/Decoder.java | 41 +- .../java/feign/codec/EncodeException.java | 10 +- core/src/main/java/feign/codec/Encoder.java | 35 +- .../main/java/feign/codec/ErrorDecoder.java | 71 ++- .../main/java/feign/codec/StringDecoder.java | 7 +- .../test/java/feign/DefaultContractTest.java | 321 +++++++----- .../test/java/feign/DefaultRetryerTest.java | 12 +- core/src/test/java/feign/EmptyTargetTest.java | 33 +- .../src/test/java/feign/FeignBuilderTest.java | 63 ++- core/src/test/java/feign/FeignTest.java | 295 +++++++---- core/src/test/java/feign/LoggerTest.java | 158 +++--- .../test/java/feign/RequestTemplateTest.java | 99 ++-- core/src/test/java/feign/UtilTest.java | 83 +-- .../java/feign/assertj/FeignAssertions.java | 4 +- .../assertj/MockWebServerAssertions.java | 2 + .../feign/assertj/RecordedRequestAssert.java | 22 +- .../feign/assertj/RequestTemplateAssert.java | 7 +- .../auth/BasicAuthRequestInterceptorTest.java | 24 +- .../java/feign/client/DefaultClientTest.java | 99 ++-- .../client/TrustingSSLSocketFactory.java | 89 ++-- .../java/feign/codec/DefaultDecoderTest.java | 31 +- .../java/feign/codec/DefaultEncoderTest.java | 21 +- .../feign/codec/DefaultErrorDecoderTest.java | 29 +- .../feign/codec/RetryAfterDecoderTest.java | 35 +- .../java/feign/examples/GitHubExample.java | 45 +- .../feign/example/github/GitHubExample.java | 25 +- .../example/wikipedia/ResponseAdapter.java | 12 +- .../example/wikipedia/WikipediaExample.java | 89 ++-- .../feign/gson/DoubleToIntMapTypeAdapter.java | 16 +- .../src/main/java/feign/gson/GsonDecoder.java | 10 +- .../src/main/java/feign/gson/GsonEncoder.java | 12 +- .../src/main/java/feign/gson/GsonFactory.java | 11 +- .../test/java/feign/gson/GsonCodecTest.java | 117 +++-- .../feign/gson/examples/GitHubExample.java | 29 +- .../java/feign/jackson/JacksonDecoder.java | 11 +- .../java/feign/jackson/JacksonEncoder.java | 16 +- .../java/feign/jackson/JacksonCodecTest.java | 173 +++--- .../feign/jackson/examples/GitHubExample.java | 34 +- .../java/feign/jaxb/JAXBContextFactory.java | 18 +- .../src/main/java/feign/jaxb/JAXBDecoder.java | 26 +- .../src/main/java/feign/jaxb/JAXBEncoder.java | 27 +- .../test/java/feign/jaxb/JAXBCodecTest.java | 129 +++-- .../feign/jaxb/JAXBContextFactoryTest.java | 43 +- .../jaxb/examples/AWSSignatureVersion4.java | 102 ++-- .../java/feign/jaxb/examples/IAMExample.java | 64 ++- .../main/java/feign/jaxrs/JAXRSContract.java | 54 +- .../java/feign/jaxrs/JAXRSContractTest.java | 492 ++++++++++++------ .../feign/jaxrs/examples/GitHubExample.java | 34 +- .../main/java/feign/okhttp/OkHttpClient.java | 81 +-- .../java/feign/okhttp/OkHttpClientTest.java | 48 +- .../src/main/java/feign/ribbon/LBClient.java | 44 +- .../feign/ribbon/LoadBalancingTarget.java | 61 ++- .../main/java/feign/ribbon/RibbonClient.java | 70 +-- .../feign/ribbon/LoadBalancingTargetTest.java | 41 +- .../java/feign/ribbon/RibbonClientTest.java | 99 ++-- sax/src/main/java/feign/sax/SAXDecoder.java | 169 +++--- .../test/java/feign/sax/SAXDecoderTest.java | 74 +-- .../sax/examples/AWSSignatureVersion4.java | 102 ++-- .../java/feign/sax/examples/IAMExample.java | 36 +- .../main/java/feign/slf4j/Slf4jLogger.java | 21 +- .../feign/slf4j/RecordingSimpleLogger.java | 31 +- .../java/feign/slf4j/Slf4jLoggerTest.java | 39 +- 89 files changed, 3426 insertions(+), 2395 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/CHANGELOG.md b/CHANGELOG.md index b5ad693186..ef53183823 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * Adds `Feign.Builder.build()` * Opens constructor for Gson and Jackson codecs which accepts type adapters * Adds EmptyTarget for interfaces who exclusively declare URI methods +* Reformats code according to [Google Java Style](https://google-styleguide.googlecode.com/svn/trunk/javaguide.html) ### Version 7.1 * Introduces feign.@Param to annotate template parameters. Users must migrate from `javax.inject.@Named` to `feign.@Param` before updating to Feign 8.0. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..d843b8d1b7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,31 @@ +# Contributing to Feign + +If you would like to contribute code you can do so through GitHub by forking the repository and sending a pull request (on a branch other than `master` or `gh-pages`). + +When submitting code, please ensure you follow the [Google Style Guide](http://google-styleguide.googlecode.com/svn/trunk/javaguide.html). For example, you can format code with Intellij using [this file](https://google-styleguide.googlecode.com/svn/trunk/intellij-java-google-style.xml). + +## License + +By contributing your code, you agree to license your contribution under the terms of the APLv2: https://github.com/Netflix/Feign/blob/master/LICENSE + +All files are released with the Apache 2.0 license. + +If you are adding a new file it should have a header like this: + +``` +/** + * 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. + */ + ``` diff --git a/core/src/main/java/feign/Body.java b/core/src/main/java/feign/Body.java index 9c3e094ed0..1c9d58a3c0 100644 --- a/core/src/main/java/feign/Body.java +++ b/core/src/main/java/feign/Body.java @@ -8,21 +8,19 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; /** - * A possibly templated body of a PUT or POST command. variables wrapped in curly braces are expanded before the - * request is submitted. - *
- * ex. - *
+ * A possibly templated body of a PUT or POST command. variables wrapped in curly braces are + * expanded before the request is submitted.
ex.
*
  * @Body("<v01:getResourceRecordsOfZone><zoneName>{zoneName}</zoneName><rrType>0</rrType></v01:getResourceRecordsOfZone>")
  * List<Record> listByZone(@Param("zoneName") String zoneName);
  * 
- *
- * Note that if you'd like curly braces literally in the body, urlencode - * them first. + *
Note that if you'd like curly braces literally in the body, urlencode them first. * * @see RequestTemplate#expand(String, Map) */ -@Target(METHOD) @Retention(RUNTIME) public @interface Body { +@Target(METHOD) +@Retention(RUNTIME) +public @interface Body { + String value(); } diff --git a/core/src/main/java/feign/Client.java b/core/src/main/java/feign/Client.java index d881177bb7..31ac3f5128 100644 --- a/core/src/main/java/feign/Client.java +++ b/core/src/main/java/feign/Client.java @@ -37,13 +37,12 @@ import static feign.Util.ENCODING_GZIP; /** - * Submits HTTP {@link Request requests}. Implementations are expected to be - * thread-safe. + * Submits HTTP {@link Request requests}. Implementations are expected to be thread-safe. */ public interface Client { + /** - * Executes a request against its {@link Request#url() url} and returns a - * response. + * Executes a request against its {@link Request#url() url} and returns a response. * * @param request safe to replay. * @param options options to apply to this request. @@ -53,22 +52,28 @@ public interface Client { Response execute(Request request, Options options) throws IOException; public static class Default implements Client { + private final SSLSocketFactory sslContextFactory; private final HostnameVerifier hostnameVerifier; - /** Null parameters imply platform defaults. */ + /** + * Null parameters imply platform defaults. + */ public Default(SSLSocketFactory sslContextFactory, HostnameVerifier hostnameVerifier) { this.sslContextFactory = sslContextFactory; this.hostnameVerifier = hostnameVerifier; } - @Override public Response execute(Request request, Options options) throws IOException { + @Override + public Response execute(Request request, Options options) throws IOException { HttpURLConnection connection = convertAndSend(request, options); return convertResponse(connection); } HttpURLConnection convertAndSend(Request request, Options options) throws IOException { - final HttpURLConnection connection = (HttpURLConnection) new URL(request.url()).openConnection(); + final HttpURLConnection + connection = + (HttpURLConnection) new URL(request.url()).openConnection(); if (connection instanceof HttpsURLConnection) { HttpsURLConnection sslCon = (HttpsURLConnection) connection; if (sslContextFactory != null) { @@ -85,12 +90,16 @@ HttpURLConnection convertAndSend(Request request, Options options) throws IOExce connection.setRequestMethod(request.method()); Collection contentEncodingValues = request.headers().get(CONTENT_ENCODING); - boolean gzipEncodedRequest = contentEncodingValues != null && contentEncodingValues.contains(ENCODING_GZIP); + boolean + gzipEncodedRequest = + contentEncodingValues != null && contentEncodingValues.contains(ENCODING_GZIP); boolean hasAcceptHeader = false; Integer contentLength = null; for (String field : request.headers().keySet()) { - if (field.equalsIgnoreCase("Accept")) hasAcceptHeader = true; + if (field.equalsIgnoreCase("Accept")) { + hasAcceptHeader = true; + } for (String value : request.headers().get(field)) { if (field.equals(CONTENT_LENGTH)) { if (!gzipEncodedRequest) { @@ -103,7 +112,9 @@ HttpURLConnection convertAndSend(Request request, Options options) throws IOExce } } // Some servers choke on the default accept string. - if (!hasAcceptHeader) connection.addRequestProperty("Accept", "*/*"); + if (!hasAcceptHeader) { + connection.addRequestProperty("Accept", "*/*"); + } if (request.body() != null) { if (contentLength != null) { @@ -135,13 +146,15 @@ Response convertResponse(HttpURLConnection connection) throws IOException { Map> headers = new LinkedHashMap>(); for (Map.Entry> field : connection.getHeaderFields().entrySet()) { // response message - if (field.getKey() != null) + if (field.getKey() != null) { headers.put(field.getKey(), field.getValue()); + } } Integer length = connection.getContentLength(); - if (length == -1) + if (length == -1) { length = null; + } InputStream stream; if (status >= 400) { stream = connection.getErrorStream(); diff --git a/core/src/main/java/feign/Contract.java b/core/src/main/java/feign/Contract.java index 931b5d1436..637f28db02 100644 --- a/core/src/main/java/feign/Contract.java +++ b/core/src/main/java/feign/Contract.java @@ -39,11 +39,13 @@ public interface Contract { abstract class BaseContract implements Contract { - @Override public List parseAndValidatateMetadata(Class declaring) { + @Override + public List parseAndValidatateMetadata(Class declaring) { List metadata = new ArrayList(); for (Method method : declaring.getDeclaredMethods()) { - if (method.getDeclaringClass() == Object.class) + if (method.getDeclaringClass() == Object.class) { continue; + } metadata.add(parseAndValidatateMetadata(method)); } return metadata; @@ -60,8 +62,9 @@ public MethodMetadata parseAndValidatateMetadata(Method method) { for (Annotation methodAnnotation : method.getAnnotations()) { processAnnotationOnMethod(data, methodAnnotation, method); } - checkState(data.template().method() != null, "Method %s not annotated with HTTP method type (ex. GET, POST)", - method.getName()); + checkState(data.template().method() != null, + "Method %s not annotated with HTTP method type (ex. GET, POST)", + method.getName()); Class[] parameterTypes = method.getParameterTypes(); Annotation[][] parameterAnnotations = method.getParameterAnnotations(); @@ -74,7 +77,8 @@ public MethodMetadata parseAndValidatateMetadata(Method method) { if (parameterTypes[i] == URI.class) { data.urlIndex(i); } else if (!isHttpAnnotation) { - checkState(data.formParams().isEmpty(), "Body parameters cannot be used with form parameters."); + checkState(data.formParams().isEmpty(), + "Body parameters cannot be used with form parameters."); checkState(data.bodyIndex() == null, "Method has too many Body parameters: %s", method); data.bodyIndex(i); data.bodyType(method.getGenericParameterTypes()[i]); @@ -88,22 +92,26 @@ public MethodMetadata parseAndValidatateMetadata(Method method) { * @param annotation annotations present on the current method annotation. * @param method method currently being processed. */ - protected abstract void processAnnotationOnMethod(MethodMetadata data, Annotation annotation, Method method); + protected abstract void processAnnotationOnMethod(MethodMetadata data, Annotation annotation, + Method method); /** * @param data metadata collected so far relating to the current java method. * @param annotations annotations present on the current parameter annotation. - * @param paramIndex if you find a name in {@code annotations}, call {@link #nameParam(MethodMetadata, String, - * int)} with this as the last parameter. - * @return true if you called {@link #nameParam(MethodMetadata, String, int)} after finding an http-relevant - * annotation. + * @param paramIndex if you find a name in {@code annotations}, call {@link + * #nameParam(MethodMetadata, String, int)} with this as the last parameter. + * @return true if you called {@link #nameParam(MethodMetadata, String, int)} after finding an + * http-relevant annotation. */ - protected abstract boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex); + protected abstract boolean processAnnotationsOnParameter(MethodMetadata data, + Annotation[] annotations, + int paramIndex); protected Collection addTemplatedParam(Collection possiblyNull, String name) { - if (possiblyNull == null) + if (possiblyNull == null) { possiblyNull = new ArrayList(); + } possiblyNull.add(String.format("{%s}", name)); return possiblyNull; } @@ -112,7 +120,9 @@ protected Collection addTemplatedParam(Collection possiblyNull, * links a parameter name to its index in the method signature. */ protected void nameParam(MethodMetadata data, String name, int i) { - Collection names = data.indexToName().containsKey(i) ? data.indexToName().get(i) : new ArrayList(); + Collection + names = + data.indexToName().containsKey(i) ? data.indexToName().get(i) : new ArrayList(); names.add(name); data.indexToName().put(i, names); } @@ -121,11 +131,13 @@ protected void nameParam(MethodMetadata data, String name, int i) { class Default extends BaseContract { @Override - protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) { + protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, + Method method) { Class annotationType = methodAnnotation.annotationType(); if (annotationType == RequestLine.class) { String requestLine = RequestLine.class.cast(methodAnnotation).value(); - checkState(emptyToNull(requestLine) != null, "RequestLine annotation was empty on method %s.", method.getName()); + checkState(emptyToNull(requestLine) != null, + "RequestLine annotation was empty on method %s.", method.getName()); if (requestLine.indexOf(' ') == -1) { data.template().method(requestLine); return; @@ -136,11 +148,13 @@ protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodA data.template().append(requestLine.substring(requestLine.indexOf(' ') + 1)); } else { // skip HTTP version - data.template().append(requestLine.substring(requestLine.indexOf(' ') + 1, requestLine.lastIndexOf(' '))); + data.template().append( + requestLine.substring(requestLine.indexOf(' ') + 1, requestLine.lastIndexOf(' '))); } } else if (annotationType == Body.class) { String body = Body.class.cast(methodAnnotation).value(); - checkState(emptyToNull(body) != null, "Body annotation was empty on method %s.", method.getName()); + checkState(emptyToNull(body) != null, "Body annotation was empty on method %s.", + method.getName()); if (body.indexOf('{') == -1) { data.template().body(body); } else { @@ -148,8 +162,11 @@ protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodA } } else if (annotationType == Headers.class) { String[] headersToParse = Headers.class.cast(methodAnnotation).value(); - checkState(headersToParse.length > 0, "Headers annotation was empty on method %s.", method.getName()); - Map> headers = new LinkedHashMap>(headersToParse.length); + checkState(headersToParse.length > 0, "Headers annotation was empty on method %s.", + method.getName()); + Map> + headers = + new LinkedHashMap>(headersToParse.length); for (String header : headersToParse) { int colon = header.indexOf(':'); String name = header.substring(0, colon); @@ -163,13 +180,15 @@ protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodA } @Override - protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) { + protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, + int paramIndex) { boolean isHttpAnnotation = false; for (Annotation annotation : annotations) { Class annotationType = annotation.annotationType(); if (annotationType == Param.class) { String name = ((Param) annotation).value(); - checkState(emptyToNull(name) != null, "Param annotation was empty on param %s.", paramIndex); + checkState(emptyToNull(name) != null, "Param annotation was empty on param %s.", + paramIndex); nameParam(data, name, paramIndex); if (annotationType == Param.class) { Class expander = ((Param) annotation).expander(); @@ -191,12 +210,14 @@ protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[ private boolean searchMapValues(Map> map, V search) { Collection> values = map.values(); - if (values == null) + if (values == null) { return false; + } for (Collection entry : values) { - if (entry.contains(search)) + if (entry.contains(search)) { return true; + } } return false; diff --git a/core/src/main/java/feign/Feign.java b/core/src/main/java/feign/Feign.java index 75318d71e3..bf23c080ce 100644 --- a/core/src/main/java/feign/Feign.java +++ b/core/src/main/java/feign/Feign.java @@ -15,6 +15,10 @@ */ package feign; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + import feign.Logger.NoOpLogger; import feign.ReflectiveFeign.ParseHandlersByName; import feign.Request.Options; @@ -22,63 +26,52 @@ import feign.codec.Decoder; import feign.codec.Encoder; import feign.codec.ErrorDecoder; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.List; /** - * Feign's purpose is to ease development against http apis that feign - * restfulness. - *
- * In implementation, Feign is a {@link Feign#newInstance factory} for - * generating {@link Target targeted} http apis. + * Feign's purpose is to ease development against http apis that feign restfulness.
In + * implementation, Feign is a {@link Feign#newInstance factory} for generating {@link Target + * targeted} http apis. */ public abstract class Feign { - /** - * Returns a new instance of an HTTP API, defined by annotations in the - * {@link Feign Contract}, for the specified {@code target}. You should - * cache this result. - */ - public abstract T newInstance(Target target); - public static Builder builder() { return new Builder(); } /** - *
- * Configuration keys are formatted as unresolved see tags. - *
- * For example. - *
    - *
  • {@code Route53}: would match a class such as - * {@code denominator.route53.Route53} - *
  • {@code Route53#list()}: would match a method such as - * {@code denominator.route53.Route53#list()} - *
  • {@code Route53#listAt(Marker)}: would match a method such as - * {@code denominator.route53.Route53#listAt(denominator.route53.Marker)} - *
  • {@code Route53#listByNameAndType(String, String)}: would match a - * method such as {@code denominator.route53.Route53#listAt(String, String)} - *
- *
- * Note that there is no whitespace expected in a key! + *
Configuration keys are formatted as unresolved see tags.
For example.
  • {@code Route53}: would match a class such as {@code + * denominator.route53.Route53}
  • {@code Route53#list()}: would match a method such as {@code + * denominator.route53.Route53#list()}
  • {@code Route53#listAt(Marker)}: would match a method + * such as {@code denominator.route53.Route53#listAt(denominator.route53.Marker)}
  • {@code + * Route53#listByNameAndType(String, String)}: would match a method such as {@code + * denominator.route53.Route53#listAt(String, String)}

Note that there is no whitespace + * expected in a key! */ public static String configKey(Method method) { StringBuilder builder = new StringBuilder(); builder.append(method.getDeclaringClass().getSimpleName()); builder.append('#').append(method.getName()).append('('); - for (Class param : method.getParameterTypes()) + for (Class param : method.getParameterTypes()) { builder.append(param.getSimpleName()).append(','); - if (method.getParameterTypes().length > 0) + } + if (method.getParameterTypes().length > 0) { builder.deleteCharAt(builder.length() - 1); + } return builder.append(')').toString(); } + /** + * Returns a new instance of an HTTP API, defined by annotations in the {@link Feign Contract}, + * for the specified {@code target}. You should cache this result. + */ + public abstract T newInstance(Target target); + public static class Builder { - private final List requestInterceptors = new ArrayList(); + + private final List + requestInterceptors = + new ArrayList(); private Logger.Level logLevel = Logger.Level.NONE; private Contract contract = new Contract.Default(); private Client client = new Client.Default(null, null); @@ -88,7 +81,9 @@ public static class Builder { private Decoder decoder = new Decoder.Default(); private ErrorDecoder errorDecoder = new ErrorDecoder.Default(); private Options options = new Options(); - private InvocationHandlerFactory invocationHandlerFactory = new InvocationHandlerFactory.Default(); + private InvocationHandlerFactory + invocationHandlerFactory = + new InvocationHandlerFactory.Default(); public Builder logLevel(Logger.Level logLevel) { this.logLevel = logLevel; @@ -144,7 +139,8 @@ public Builder requestInterceptor(RequestInterceptor requestInterceptor) { } /** - * Sets the full set of request interceptors for the builder, overwriting any previous interceptors. + * Sets the full set of request interceptors for the builder, overwriting any previous + * interceptors. */ public Builder requestInterceptors(Iterable requestInterceptors) { this.requestInterceptors.clear(); @@ -154,7 +150,9 @@ public Builder requestInterceptors(Iterable requestIntercept return this; } - /** Allows you to override how reflective dispatch works inside of Feign. */ + /** + * Allows you to override how reflective dispatch works inside of Feign. + */ public Builder invocationHandlerFactory(InvocationHandlerFactory invocationHandlerFactory) { this.invocationHandlerFactory = invocationHandlerFactory; return this; @@ -170,9 +168,12 @@ public T target(Target target) { public Feign build() { SynchronousMethodHandler.Factory synchronousMethodHandlerFactory = - new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger, logLevel); - ParseHandlersByName handlersByName = new ParseHandlersByName( contract, options, encoder, decoder, - errorDecoder, synchronousMethodHandlerFactory); + new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger, + logLevel); + ParseHandlersByName + handlersByName = + new ParseHandlersByName(contract, options, encoder, decoder, + errorDecoder, synchronousMethodHandlerFactory); return new ReflectiveFeign(handlersByName, invocationHandlerFactory); } } diff --git a/core/src/main/java/feign/FeignException.java b/core/src/main/java/feign/FeignException.java index b014d71130..397f8c9500 100644 --- a/core/src/main/java/feign/FeignException.java +++ b/core/src/main/java/feign/FeignException.java @@ -15,16 +15,28 @@ */ package feign; -import static java.lang.String.format; - import java.io.IOException; +import static java.lang.String.format; + /** * Origin exception type for all Http Apis. */ public class FeignException extends RuntimeException { + + private static final long serialVersionUID = 0; + + protected FeignException(String message, Throwable cause) { + super(message, cause); + } + + protected FeignException(String message) { + super(message); + } + static FeignException errorReading(Request request, Response ignored, IOException cause) { - return new FeignException(format("%s %s %s", cause.getMessage(), request.method(), request.url()), cause); + return new FeignException( + format("%s %s %s", cause.getMessage(), request.method(), request.url()), cause); } public static FeignException errorStatus(String methodKey, Response response) { @@ -40,17 +52,8 @@ public static FeignException errorStatus(String methodKey, Response response) { } static FeignException errorExecuting(Request request, IOException cause) { - return new RetryableException(format("error %s executing %s %s", cause.getMessage(), request.method(), - request.url()), cause, null); - } - - protected FeignException(String message, Throwable cause) { - super(message, cause); + return new RetryableException( + format("error %s executing %s %s", cause.getMessage(), request.method(), + request.url()), cause, null); } - - protected FeignException(String message) { - super(message); - } - - private static final long serialVersionUID = 0; } diff --git a/core/src/main/java/feign/Headers.java b/core/src/main/java/feign/Headers.java index b250fb65fa..2b7161cfb2 100644 --- a/core/src/main/java/feign/Headers.java +++ b/core/src/main/java/feign/Headers.java @@ -7,8 +7,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; /** - * Expands headers supplied in the {@code value}. Variables are permitted as values. - *
+ * Expands headers supplied in the {@code value}. Variables are permitted as values.
*
  * @RequestLine("GET /")
  * @Headers("Cache-Control: max-age=640000")
@@ -21,14 +20,9 @@
  * }) void post(@Param("token") String token);
  * ...
  * 
- *
- * Note: Headers do not overwrite each other. All headers with the same name will - * be included in the request. - *

Relationship to JAXRS
- *
- * The following two forms are identical. - *
- * Feign: + *
Note: Headers do not overwrite each other. All headers with the same name + * will be included in the request.

Relationship to JAXRS

The following two + * forms are identical.
Feign: *
  * @RequestLine("POST /")
  * @Headers({
@@ -36,15 +30,16 @@
  * }) void post(@Named("token") String token);
  * ...
  * 
- *
- * JAX-RS: + *
JAX-RS: *
  * @POST @Path("/")
  * void post(@HeaderParam("X-Ping") String token);
  * ...
  * 
*/ -@Target(METHOD) @Retention(RUNTIME) +@Target(METHOD) +@Retention(RUNTIME) public @interface Headers { + String[] value(); } diff --git a/core/src/main/java/feign/InvocationHandlerFactory.java b/core/src/main/java/feign/InvocationHandlerFactory.java index 7dabf77ded..1df508b079 100644 --- a/core/src/main/java/feign/InvocationHandlerFactory.java +++ b/core/src/main/java/feign/InvocationHandlerFactory.java @@ -19,17 +19,24 @@ import java.lang.reflect.Method; import java.util.Map; -/** Controls reflective method dispatch. */ +/** + * Controls reflective method dispatch. + */ public interface InvocationHandlerFactory { - /** Like {@link InvocationHandler#invoke(Object, java.lang.reflect.Method, Object[])}, except for a single method. */ + InvocationHandler create(Target target, Map dispatch); + + /** + * Like {@link InvocationHandler#invoke(Object, java.lang.reflect.Method, Object[])}, except for a + * single method. + */ interface MethodHandler { + Object invoke(Object[] argv) throws Throwable; } - InvocationHandler create(Target target, Map dispatch); - static final class Default implements InvocationHandlerFactory { + @Override public InvocationHandler create(Target target, Map dispatch) { return new ReflectiveFeign.FeignInvocationHandler(target, dispatch); diff --git a/core/src/main/java/feign/Logger.java b/core/src/main/java/feign/Logger.java index c693f68eb1..474786a3a3 100644 --- a/core/src/main/java/feign/Logger.java +++ b/core/src/main/java/feign/Logger.java @@ -22,8 +22,8 @@ import java.util.logging.LogRecord; import java.util.logging.SimpleFormatter; -import static feign.Util.decodeOrDefault; import static feign.Util.UTF_8; +import static feign.Util.decodeOrDefault; import static feign.Util.valuesOrEmpty; /** @@ -31,6 +31,92 @@ */ public abstract class Logger { + protected static String methodTag(String configKey) { + return new StringBuilder().append('[').append(configKey.substring(0, configKey.indexOf('('))) + .append("] ").toString(); + } + + /** + * Override to log requests and responses using your own implementation. Messages will be http + * request and response text. + * + * @param configKey value of {@link Feign#configKey(java.lang.reflect.Method)} + * @param format {@link java.util.Formatter format string} + * @param args arguments applied to {@code format} + */ + protected abstract void log(String configKey, String format, Object... args); + + protected void logRequest(String configKey, Level logLevel, Request request) { + log(configKey, "---> %s %s HTTP/1.1", request.method(), request.url()); + if (logLevel.ordinal() >= Level.HEADERS.ordinal()) { + + for (String field : request.headers().keySet()) { + for (String value : valuesOrEmpty(request.headers(), field)) { + log(configKey, "%s: %s", field, value); + } + } + + int bodyLength = 0; + if (request.body() != null) { + bodyLength = request.body().length; + if (logLevel.ordinal() >= Level.FULL.ordinal()) { + String + bodyText = + request.charset() != null ? new String(request.body(), request.charset()) : null; + log(configKey, ""); // CRLF + log(configKey, "%s", bodyText != null ? bodyText : "Binary data"); + } + } + log(configKey, "---> END HTTP (%s-byte body)", bodyLength); + } + } + + void logRetry(String configKey, Level logLevel) { + log(configKey, "---> RETRYING"); + } + + protected 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(configKey, "%s: %s", field, value); + } + } + + int bodyLength = 0; + if (response.body() != null) { + if (logLevel.ordinal() >= Level.FULL.ordinal()) { + log(configKey, ""); // CRLF + } + byte[] bodyData = Util.toByteArray(response.body().asInputStream()); + bodyLength = bodyData.length; + if (logLevel.ordinal() >= Level.FULL.ordinal() && bodyLength > 0) { + log(configKey, "%s", decodeOrDefault(bodyData, UTF_8, "Binary data")); + } + log(configKey, "<--- END HTTP (%s-byte body)", bodyLength); + return Response.create(response.status(), response.reason(), response.headers(), bodyData); + } else { + log(configKey, "<--- END HTTP (%s-byte body)", bodyLength); + } + } + 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; + } + /** * Controls the level of logging. */ @@ -57,9 +143,13 @@ public enum Level { * logs to the category {@link Logger} at {@link java.util.logging.Level#FINE}. */ public static class ErrorLogger extends Logger { - final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(Logger.class.getName()); - @Override protected void log(String configKey, String format, Object... args) { + final java.util.logging.Logger + logger = + java.util.logging.Logger.getLogger(Logger.class.getName()); + + @Override + protected void log(String configKey, String format, Object... args) { System.err.printf(methodTag(configKey) + format + "%n", args); } } @@ -68,27 +158,35 @@ public static class ErrorLogger extends Logger { * logs to the category {@link Logger} at {@link java.util.logging.Level#FINE}, if loggable. */ public static class JavaLogger extends Logger { - final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(Logger.class.getName()); - @Override protected void logRequest(String configKey, Level logLevel, Request request) { + final java.util.logging.Logger + logger = + java.util.logging.Logger.getLogger(Logger.class.getName()); + + @Override + protected void logRequest(String configKey, Level logLevel, Request request) { if (logger.isLoggable(java.util.logging.Level.FINE)) { super.logRequest(configKey, logLevel, request); } } - @Override protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime) throws IOException { + @Override + protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response, + long elapsedTime) throws IOException { if (logger.isLoggable(java.util.logging.Level.FINE)) { return super.logAndRebufferResponse(configKey, logLevel, response, elapsedTime); } return response; } - @Override protected void log(String configKey, String format, Object... args) { + @Override + protected void log(String configKey, String format, Object... args) { logger.fine(String.format(methodTag(configKey) + format, args)); } /** - * helper that configures jul to sanely log messages at FINE level without additional formatting. + * helper that configures jul to sanely log messages at FINE level without additional + * formatting. */ public JavaLogger appendToFile(String logfile) { logger.setLevel(java.util.logging.Level.FINE); @@ -109,95 +207,19 @@ public String format(LogRecord record) { } public static class NoOpLogger extends Logger { - @Override protected void logRequest(String configKey, Level logLevel, Request request) { - } - - @Override protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime) throws IOException { - return response; - } - @Override protected void log(String configKey, String format, Object... args) { + @Override + protected void logRequest(String configKey, Level logLevel, Request request) { } - } - - /** - * Override to log requests and responses using your own implementation. - * Messages will be http request and response text. - * - * @param configKey value of {@link Feign#configKey(java.lang.reflect.Method)} - * @param format {@link java.util.Formatter format string} - * @param args arguments applied to {@code format} - */ - protected abstract void log(String configKey, String format, Object... args); - - protected void logRequest(String configKey, Level logLevel, Request request) { - log(configKey, "---> %s %s HTTP/1.1", request.method(), request.url()); - if (logLevel.ordinal() >= Level.HEADERS.ordinal()) { - for (String field : request.headers().keySet()) { - for (String value : valuesOrEmpty(request.headers(), field)) { - log(configKey, "%s: %s", field, value); - } - } - - int bodyLength = 0; - if (request.body() != null) { - bodyLength = request.body().length; - if (logLevel.ordinal() >= Level.FULL.ordinal()) { - String bodyText = request.charset() != null ? new String(request.body(), request.charset()) : null; - log(configKey, ""); // CRLF - log(configKey, "%s", bodyText != null ? bodyText : "Binary data"); - } - } - log(configKey, "---> END HTTP (%s-byte body)", bodyLength); - } - } - - void logRetry(String configKey, Level logLevel) { - log(configKey, "---> RETRYING"); - } - - protected 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(configKey, "%s: %s", field, value); - } - } - - int bodyLength = 0; - if (response.body() != null) { - if (logLevel.ordinal() >= Level.FULL.ordinal()) { - log(configKey, ""); // CRLF - } - byte[] bodyData = Util.toByteArray(response.body().asInputStream()); - bodyLength = bodyData.length; - if (logLevel.ordinal() >= Level.FULL.ordinal() && bodyLength > 0) { - log(configKey, "%s", decodeOrDefault(bodyData, UTF_8, "Binary data")); - } - log(configKey, "<--- END HTTP (%s-byte body)", bodyLength); - return Response.create(response.status(), response.reason(), response.headers(), bodyData); - } else { - log(configKey, "<--- END HTTP (%s-byte body)", bodyLength); - } + @Override + protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response, + long elapsedTime) throws IOException { + return response; } - 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"); + @Override + protected void log(String configKey, String format, Object... args) { } - return ioe; - } - - protected static String methodTag(String configKey) { - return new StringBuilder().append('[').append(configKey.substring(0, configKey.indexOf('('))).append("] ").toString(); } } diff --git a/core/src/main/java/feign/MethodMetadata.java b/core/src/main/java/feign/MethodMetadata.java index 61bbc38a94..ef2af470d4 100644 --- a/core/src/main/java/feign/MethodMetadata.java +++ b/core/src/main/java/feign/MethodMetadata.java @@ -15,7 +15,6 @@ */ package feign; -import feign.Param.Expander; import java.io.Serializable; import java.lang.reflect.Type; import java.util.ArrayList; @@ -24,11 +23,11 @@ import java.util.List; import java.util.Map; -public final class MethodMetadata implements Serializable { +import feign.Param.Expander; - MethodMetadata() { - } +public final class MethodMetadata implements Serializable { + private static final long serialVersionUID = 1L; private String configKey; private transient Type returnType; private Integer urlIndex; @@ -36,10 +35,15 @@ public final class MethodMetadata implements Serializable { private transient Type bodyType; private RequestTemplate template = new RequestTemplate(); private List formParams = new ArrayList(); - private Map> indexToName = new LinkedHashMap>(); + private Map> + indexToName = + new LinkedHashMap>(); private Map> indexToExpanderClass = new LinkedHashMap>(); + MethodMetadata() { + } + /** * @see Feign#configKey(java.lang.reflect.Method) */ @@ -79,7 +83,9 @@ MethodMetadata bodyIndex(Integer bodyIndex) { return this; } - /** Type corresponding to {@link #bodyIndex()}. */ + /** + * Type corresponding to {@link #bodyIndex()}. + */ public Type bodyType() { return bodyType; } @@ -104,6 +110,4 @@ public Map> indexToName() { public Map> indexToExpanderClass() { return indexToExpanderClass; } - - private static final long serialVersionUID = 1L; } diff --git a/core/src/main/java/feign/Param.java b/core/src/main/java/feign/Param.java index e26964feee..46c4ede7cb 100644 --- a/core/src/main/java/feign/Param.java +++ b/core/src/main/java/feign/Param.java @@ -20,23 +20,36 @@ import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.RetentionPolicy.RUNTIME; -/** A named template parameter applied to {@link Headers}, {@linkplain RequestLine} or {@linkplain Body} */ +/** + * A named template parameter applied to {@link Headers}, {@linkplain RequestLine} or {@linkplain + * Body} + */ @Retention(RUNTIME) @java.lang.annotation.Target(PARAMETER) public @interface Param { - /** The name of the template parameter. */ + + /** + * The name of the template parameter. + */ String value(); - /** How to expand the value of this parameter, if {@link ToStringExpander} isn't adequate. */ + /** + * How to expand the value of this parameter, if {@link ToStringExpander} isn't adequate. + */ Class expander() default ToStringExpander.class; interface Expander { - /** Expands the value into a string. Does not accept or return null. */ + + /** + * Expands the value into a string. Does not accept or return null. + */ String expand(Object value); } final class ToStringExpander implements Expander { - @Override public String expand(Object value) { + + @Override + public String expand(Object value) { return value.toString(); } } diff --git a/core/src/main/java/feign/ReflectiveFeign.java b/core/src/main/java/feign/ReflectiveFeign.java index bbb1ac0d50..b901d9c9d3 100644 --- a/core/src/main/java/feign/ReflectiveFeign.java +++ b/core/src/main/java/feign/ReflectiveFeign.java @@ -15,14 +15,6 @@ */ package feign; -import feign.InvocationHandlerFactory.MethodHandler; -import feign.Param.Expander; -import feign.Request.Options; -import feign.codec.Decoder; -import feign.codec.EncodeException; -import feign.codec.Encoder; -import feign.codec.ErrorDecoder; - import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; @@ -32,6 +24,14 @@ import java.util.Map; import java.util.Map.Entry; +import feign.InvocationHandlerFactory.MethodHandler; +import feign.Param.Expander; +import feign.Request.Options; +import feign.codec.Decoder; +import feign.codec.EncodeException; +import feign.codec.Encoder; +import feign.codec.ErrorDecoder; + import static feign.Util.checkArgument; import static feign.Util.checkNotNull; @@ -46,19 +46,23 @@ public class ReflectiveFeign extends Feign { } /** - * creates an api binding to the {@code target}. As this invokes reflection, - * care should be taken to cache the result. + * creates an api binding to the {@code target}. As this invokes reflection, care should be taken + * to cache the result. */ - @SuppressWarnings("unchecked") @Override public T newInstance(Target target) { + @SuppressWarnings("unchecked") + @Override + public T newInstance(Target target) { Map nameToHandler = targetToHandlersByName.apply(target); Map methodToHandler = new LinkedHashMap(); for (Method method : target.type().getDeclaredMethods()) { - if (method.getDeclaringClass() == Object.class) + if (method.getDeclaringClass() == Object.class) { continue; + } methodToHandler.put(method, nameToHandler.get(Feign.configKey(method))); } InvocationHandler handler = factory.create(target, methodToHandler); - return (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class[]{target.type()}, handler); + return (T) Proxy + .newProxyInstance(target.type().getClassLoader(), new Class[]{target.type()}, handler); } static class FeignInvocationHandler implements InvocationHandler { @@ -71,10 +75,13 @@ static class FeignInvocationHandler implements InvocationHandler { this.dispatch = checkNotNull(dispatch, "dispatch for %s", target); } - @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + @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; + Object + otherHandler = + args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null; return equals(otherHandler); } catch (IllegalArgumentException e) { return false; @@ -87,7 +94,8 @@ static class FeignInvocationHandler implements InvocationHandler { return dispatch.get(method).invoke(args); } - @Override public boolean equals(Object obj) { + @Override + public boolean equals(Object obj) { if (obj instanceof FeignInvocationHandler) { FeignInvocationHandler other = (FeignInvocationHandler) obj; return target.equals(other.target); @@ -95,16 +103,19 @@ static class FeignInvocationHandler implements InvocationHandler { return false; } - @Override public int hashCode() { + @Override + public int hashCode() { return target.hashCode(); } - @Override public String toString() { + @Override + public String toString() { return target.toString(); } } static final class ParseHandlersByName { + private final Contract contract; private final Options options; private final Encoder encoder; @@ -134,22 +145,28 @@ public Map apply(Target key) { } else { buildTemplate = new BuildTemplateByResolvingArgs(md); } - result.put(md.configKey(), factory.create(key, md, buildTemplate, options, decoder, errorDecoder)); + result.put(md.configKey(), + factory.create(key, md, buildTemplate, options, decoder, errorDecoder)); } return result; } } private static class BuildTemplateByResolvingArgs implements RequestTemplate.Factory { + protected final MethodMetadata metadata; private final Map indexToExpander = new LinkedHashMap(); private BuildTemplateByResolvingArgs(MethodMetadata metadata) { this.metadata = metadata; - if (metadata.indexToExpanderClass().isEmpty()) return; - for (Entry> indexToExpanderClass : metadata.indexToExpanderClass().entrySet()) { + if (metadata.indexToExpanderClass().isEmpty()) { + return; + } + for (Entry> indexToExpanderClass : metadata + .indexToExpanderClass().entrySet()) { try { - indexToExpander.put(indexToExpanderClass.getKey(), indexToExpanderClass.getValue().newInstance()); + indexToExpander + .put(indexToExpanderClass.getKey(), indexToExpanderClass.getValue().newInstance()); } catch (InstantiationException e) { throw new IllegalStateException(e); } catch (IllegalAccessException e) { @@ -158,7 +175,8 @@ private BuildTemplateByResolvingArgs(MethodMetadata metadata) { } } - @Override public RequestTemplate create(Object[] argv) { + @Override + public RequestTemplate create(Object[] argv) { RequestTemplate mutable = new RequestTemplate(metadata.template()); if (metadata.urlIndex() != null) { int urlIndex = metadata.urlIndex(); @@ -173,19 +191,22 @@ private BuildTemplateByResolvingArgs(MethodMetadata metadata) { if (indexToExpander.containsKey(i)) { value = indexToExpander.get(i).expand(value); } - for (String name : entry.getValue()) + for (String name : entry.getValue()) { varBuilder.put(name, value); + } } } return resolve(argv, mutable, varBuilder); } - protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map variables) { + protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, + Map variables) { return mutable.resolve(variables); } } private static class BuildFormEncodedTemplateFromArgs extends BuildTemplateByResolvingArgs { + private final Encoder encoder; private BuildFormEncodedTemplateFromArgs(MethodMetadata metadata, Encoder encoder) { @@ -194,11 +215,13 @@ private BuildFormEncodedTemplateFromArgs(MethodMetadata metadata, Encoder encode } @Override - protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map variables) { + protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, + Map variables) { Map formVariables = new LinkedHashMap(); for (Entry entry : variables.entrySet()) { - if (metadata.formParams().contains(entry.getKey())) + if (metadata.formParams().contains(entry.getKey())) { formVariables.put(entry.getKey(), entry.getValue()); + } } try { encoder.encode(formVariables, Types.MAP_STRING_WILDCARD, mutable); @@ -212,6 +235,7 @@ protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map variables) { + protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, + Map variables) { Object body = argv[metadata.bodyIndex()]; checkArgument(body != null, "Body parameter %s was null", metadata.bodyIndex()); try { diff --git a/core/src/main/java/feign/Request.java b/core/src/main/java/feign/Request.java index 76d0f54f59..823d85f7c3 100644 --- a/core/src/main/java/feign/Request.java +++ b/core/src/main/java/feign/Request.java @@ -35,10 +35,13 @@ public final class Request { private final byte[] body; private final Charset charset; - Request(String method, String url, Map> headers, byte[] body, Charset charset) { + Request(String method, String url, Map> headers, byte[] body, + Charset charset) { this.method = checkNotNull(method, "method of %s", url); this.url = checkNotNull(url, "url"); - LinkedHashMap> copyOf = new LinkedHashMap>(); + LinkedHashMap> + copyOf = + new LinkedHashMap>(); copyOf.putAll(checkNotNull(headers, "headers of %s %s", method, url)); this.headers = Collections.unmodifiableMap(copyOf); this.body = body; // nullable @@ -61,15 +64,17 @@ public Map> headers() { } /** - * The character set with which the body is encoded, or null if unknown or not applicable. When this is - * present, you can use {@code new String(req.body(), req.charset())} to access the body as a String. + * The character set with which the body is encoded, or null if unknown or not applicable. When + * this is present, you can use {@code new String(req.body(), req.charset())} to access the body + * as a String. */ public Charset charset() { return charset; } /** - * If present, this is the replayable body to send to the server. In some cases, this may be interpretable as text. + * If present, this is the replayable body to send to the server. In some cases, this may be + * interpretable as text. * * @see #charset() */ @@ -77,6 +82,21 @@ public byte[] body() { return body; } + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(method).append(' ').append(url).append(" HTTP/1.1\n"); + for (String field : headers.keySet()) { + for (String value : valuesOrEmpty(headers, field)) { + builder.append(field).append(": ").append(value).append('\n'); + } + } + if (body != null) { + builder.append('\n').append(charset != null ? new String(body, charset) : "Binary data"); + } + return builder.toString(); + } + /* Controls the per-request settings currently required to be implemented by all {@link Client clients} */ public static class Options { @@ -110,18 +130,4 @@ public int readTimeoutMillis() { return readTimeoutMillis; } } - - @Override public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append(method).append(' ').append(url).append(" HTTP/1.1\n"); - for (String field : headers.keySet()) { - for (String value : valuesOrEmpty(headers, field)) { - builder.append(field).append(": ").append(value).append('\n'); - } - } - if (body != null) { - builder.append('\n').append(charset != null ? new String(body, charset) : "Binary data"); - } - return builder.toString(); - } } diff --git a/core/src/main/java/feign/RequestInterceptor.java b/core/src/main/java/feign/RequestInterceptor.java index 0c4ad016fc..7378bcaaac 100644 --- a/core/src/main/java/feign/RequestInterceptor.java +++ b/core/src/main/java/feign/RequestInterceptor.java @@ -16,41 +16,27 @@ 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)}. - *
- *
- * For example: - *
+ * 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)}.

+ * For example:
*
  * public void apply(RequestTemplate input) {
  *     input.replaceHeader("X-Auth", currentToken);
  * }
  * 
- *
- *
Configuration
- *
- * {@code RequestInterceptors} are configured via {@link Feign.Builder#requestInterceptors}. - *
- *
Implementation notes
- *
- * Do not add parameters, such as {@code /path/{foo}/bar } - * in your implementation of {@link #apply(RequestTemplate)}. - *
- * Interceptors are applied after the template's parameters are - * {@link RequestTemplate#resolve(java.util.Map) resolved}. This is to ensure - * that you can implement signatures are interceptors. - *
- *

Relationship to Retrofit 1.x
- *
- * This class is similar to {@code RequestInterceptor.intercept()}, - * except that the implementation can read, remove, or otherwise mutate any - * part of the request template. + *

Configuration

{@code RequestInterceptors} are configured via {@link + * Feign.Builder#requestInterceptors}.

Implementation notes

Do not add + * parameters, such as {@code /path/{foo}/bar } in your implementation of {@link + * #apply(RequestTemplate)}.
Interceptors are applied after the template's parameters are + * {@link RequestTemplate#resolve(java.util.Map) resolved}. This is to ensure that you can + * implement signatures are interceptors.


Relationship to Retrofit 1.x

+ * This class is similar to {@code RequestInterceptor.intercept()}, except that the implementation + * can read, remove, or otherwise mutate any part of the request template. */ public interface RequestInterceptor { + /** * Called for every request. Add data using methods on the supplied {@link RequestTemplate}. */ diff --git a/core/src/main/java/feign/RequestLine.java b/core/src/main/java/feign/RequestLine.java index 14c1d68005..36b1bb2d6b 100644 --- a/core/src/main/java/feign/RequestLine.java +++ b/core/src/main/java/feign/RequestLine.java @@ -6,9 +6,8 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; /** - * Expands the request-line supplied in the {@code value}, permitting path and query variables, - * or just the http method. - *
+ * Expands the request-line supplied in the {@code value}, permitting path and query variables, or + * just the http method.
*
  * ...
  * @RequestLine("POST /servers")
@@ -22,35 +21,30 @@
  * Response getNext(URI nextLink);
  * ...
  * 
- * HTTP version suffix is optional, but permitted. There are no guarantees this version will impact that - * sent by the client. - *
+ * HTTP version suffix is optional, but permitted. There are no guarantees this version will impact + * that sent by the client.
*
  * @RequestLine("POST /servers HTTP/1.1")
  * ...
  * 
- *
- * Note: Query params do not overwrite each other. All queries with the same name will - * be included in the request. - *

Relationship to JAXRS
- *
- * The following two forms are identical. - *
- * Feign: + *
Note: Query params do not overwrite each other. All queries with the same + * name will be included in the request.

Relationship to JAXRS

The following + * two forms are identical.
Feign: *
  * @RequestLine("GET /servers/{serverId}?count={count}")
  * void get(@Param("serverId") String serverId, @Param("count") int count);
  * ...
  * 
- *
- * JAX-RS: + *
JAX-RS: *
  * @GET @Path("/servers/{serverId}")
  * void get(@PathParam("serverId") String serverId, @QueryParam("count") int count);
  * ...
  * 
*/ -@java.lang.annotation.Target(METHOD) @Retention(RUNTIME) +@java.lang.annotation.Target(METHOD) +@Retention(RUNTIME) public @interface RequestLine { + String value(); } diff --git a/core/src/main/java/feign/RequestTemplate.java b/core/src/main/java/feign/RequestTemplate.java index 081369d569..2574f0c2a7 100644 --- a/core/src/main/java/feign/RequestTemplate.java +++ b/core/src/main/java/feign/RequestTemplate.java @@ -38,27 +38,23 @@ import static feign.Util.valuesOrEmpty; /** - * Builds a request to an http target. Not thread safe. - *
- *

relationship to JAXRS 2.0
- *
- * A combination of {@code javax.ws.rs.client.WebTarget} and - * {@code javax.ws.rs.client.Invocation.Builder}, ensuring you can modify any - * part of the request. However, this object is mutable, so needs to be guarded - * with the copy constructor. + * Builds a request to an http target. Not thread safe.


relationship to JAXRS + * 2.0

A combination of {@code javax.ws.rs.client.WebTarget} and {@code + * javax.ws.rs.client.Invocation.Builder}, ensuring you can modify any part of the request. However, + * this object is mutable, so needs to be guarded with the copy constructor. */ public final class RequestTemplate implements Serializable { - interface Factory { - /** create a request template using args passed to a method invocation. */ - RequestTemplate create(Object[] argv); - } - + private static final long serialVersionUID = 1L; + private final Map> + queries = + new LinkedHashMap>(); + private final Map> + headers = + new LinkedHashMap>(); private String method; /* final to encourage mutable use vs replacing the object. */ private StringBuilder url = new StringBuilder(); - private final Map> queries = new LinkedHashMap>(); - private final Map> headers = new LinkedHashMap>(); private transient Charset charset; private byte[] body; private String bodyTemplate; @@ -79,54 +75,6 @@ public RequestTemplate(RequestTemplate toCopy) { this.bodyTemplate = toCopy.bodyTemplate; } - /** - * Resolves any template parameters in the requests path, query, or headers - * against the supplied unencoded arguments. - *
- *

relationship to JAXRS 2.0
- *
- * This call is similar to - * {@code javax.ws.rs.client.WebTarget.resolveTemplates(templateValues, true)} - * , except that the template values apply to any part of the request, not - * just the URL - */ - public RequestTemplate resolve(Map unencoded) { - replaceQueryValues(unencoded); - Map encoded = new LinkedHashMap(); - for (Entry entry : unencoded.entrySet()) { - encoded.put(entry.getKey(), urlEncode(String.valueOf(entry.getValue()))); - } - String resolvedUrl = expand(url.toString(), encoded).replace("%2F", "/"); - url = new StringBuilder(resolvedUrl); - - Map> resolvedHeaders = new LinkedHashMap>(); - for (String field : headers.keySet()) { - Collection resolvedValues = new ArrayList(); - for (String value : valuesOrEmpty(headers, field)) { - String resolved; - if (value.indexOf('{') == 0) { - resolved = String.valueOf(unencoded.get(field)); - } else { - resolved = value; - } - if (resolved != null) - resolvedValues.add(resolved); - } - resolvedHeaders.put(field, resolvedValues); - } - headers.clear(); - headers.putAll(resolvedHeaders); - if (bodyTemplate != null) - body(urlDecode(expand(bodyTemplate, unencoded))); - return this; - } - - /* roughly analogous to {@code javax.ws.rs.client.Target.request()}. */ - public Request request() { - return new Request(method, new StringBuilder(url).append(queryLine()).toString(), - headers, body, charset); - } - private static String urlDecode(String arg) { try { return URLDecoder.decode(arg, UTF_8.name()); @@ -144,22 +92,21 @@ private static String urlEncode(Object arg) { } /** - * Expands a {@code template}, such as {@code username}, using the {@code variables} supplied. Any unresolved - * parameters will remain. - *
- * Note that if you'd like curly braces literally in the {@code template}, - * urlencode them first. + * Expands a {@code template}, such as {@code username}, using the {@code variables} supplied. Any + * unresolved parameters will remain.
Note that if you'd like curly braces literally in the + * {@code template}, urlencode them first. * - * @param template URI template that can be in level 1 RFC6570 form. + * @param template URI template that can be in level 1 RFC6570 + * form. * @param variables to the URI template * @return expanded template, leaving any unresolved parameters literal */ public static String expand(String template, Map variables) { // skip expansion if there's no valid variables set. ex. {a} is the // first valid - if (checkNotNull(template, "template").length() < 3) + if (checkNotNull(template, "template").length() < 3) { return template.toString(); + } checkNotNull(variables, "variables for %s", template); boolean inVar = false; @@ -174,22 +121,114 @@ public static String expand(String template, Map variables) { inVar = false; String key = var.toString(); Object value = variables.get(var.toString()); - if (value != null) + if (value != null) { builder.append(value); - else + } else { builder.append('{').append(key).append('}'); + } var = new StringBuilder(); break; default: - if (inVar) + if (inVar) { var.append(c); - else + } else { builder.append(c); + } } } return builder.toString(); } + private static Map> parseAndDecodeQueries(String queryLine) { + Map> map = new LinkedHashMap>(); + if (emptyToNull(queryLine) == null) { + return map; + } + if (queryLine.indexOf('&') == -1) { + if (queryLine.indexOf('=') != -1) { + putKV(queryLine, map); + } else { + map.put(queryLine, null); + } + } else { + char[] chars = queryLine.toCharArray(); + int start = 0; + int i = 0; + for (; i < chars.length; i++) { + if (chars[i] == '&') { + putKV(queryLine.substring(start, i), map); + start = i + 1; + } + } + putKV(queryLine.substring(start, i), map); + } + return map; + } + + private static void putKV(String stringToParse, Map> map) { + String key; + String value; + // note that '=' can be a valid part of the value + int firstEq = stringToParse.indexOf('='); + if (firstEq == -1) { + key = urlDecode(stringToParse); + value = null; + } else { + key = urlDecode(stringToParse.substring(0, firstEq)); + value = urlDecode(stringToParse.substring(firstEq + 1)); + } + Collection values = map.containsKey(key) ? map.get(key) : new ArrayList(); + values.add(value); + map.put(key, values); + } + + /** + * Resolves any template parameters in the requests path, query, or headers against the supplied + * unencoded arguments.


relationship to JAXRS 2.0

This call is + * similar to {@code javax.ws.rs.client.WebTarget.resolveTemplates(templateValues, true)} , except + * that the template values apply to any part of the request, not just the URL + */ + public RequestTemplate resolve(Map unencoded) { + replaceQueryValues(unencoded); + Map encoded = new LinkedHashMap(); + for (Entry entry : unencoded.entrySet()) { + encoded.put(entry.getKey(), urlEncode(String.valueOf(entry.getValue()))); + } + String resolvedUrl = expand(url.toString(), encoded).replace("%2F", "/"); + url = new StringBuilder(resolvedUrl); + + Map> + resolvedHeaders = + new LinkedHashMap>(); + for (String field : headers.keySet()) { + Collection resolvedValues = new ArrayList(); + for (String value : valuesOrEmpty(headers, field)) { + String resolved; + if (value.indexOf('{') == 0) { + resolved = String.valueOf(unencoded.get(field)); + } else { + resolved = value; + } + if (resolved != null) { + resolvedValues.add(resolved); + } + } + resolvedHeaders.put(field, resolvedValues); + } + headers.clear(); + headers.putAll(resolvedHeaders); + if (bodyTemplate != null) { + body(urlDecode(expand(bodyTemplate, unencoded))); + } + return this; + } + + /* roughly analogous to {@code javax.ws.rs.client.Target.request()}. */ + public Request request() { + return new Request(method, new StringBuilder(url).append(queryLine()).toString(), + headers, body, charset); + } + /* @see Request#method() */ public RequestTemplate method(String method) { this.method = checkNotNull(method, "method"); @@ -219,25 +258,17 @@ public String url() { } /** - * Replaces queries with the specified {@code configKey} with url decoded - * {@code values} supplied. - *
- * When the {@code value} is {@code null}, all queries with the {@code configKey} - * are removed. - *
- *

relationship to JAXRS 2.0
- *
- * Like {@code WebTarget.query}, except the values can be templatized. - *
- * ex. - *
+ * Replaces queries with the specified {@code configKey} with url decoded {@code values} supplied. + *
When the {@code value} is {@code null}, all queries with the {@code configKey} are + * removed.


relationship to JAXRS 2.0

Like {@code WebTarget.query}, + * except the values can be templatized.
ex.
*
    * template.query("Signature", "{signature}");
    * 
* * @param configKey the configKey of the query - * @param values can be a single null to imply removing all values. Else no - * values are expected to be null. + * @param values can be a single null to imply removing all values. Else no values are expected + * to be null. * @see #queries() */ public RequestTemplate query(String configKey, String... values) { @@ -254,41 +285,37 @@ public RequestTemplate query(String configKey, String... values) { /* @see #query(String, String...) */ public RequestTemplate query(String configKey, Iterable values) { - if (values != null) + if (values != null) { return query(configKey, toArray(values, String.class)); + } return query(configKey, (String[]) null); } private String encodeIfNotVariable(String in) { - if (in == null || in.indexOf('{') == 0) + if (in == null || in.indexOf('{') == 0) { return in; + } return urlEncode(in); } /** - * Replaces all existing queries with the newly supplied url decoded - * queries. - *
- *

relationship to JAXRS 2.0
- *
- * Like {@code WebTarget.queries}, except the values can be templatized. - *
- * ex. - *
+ * Replaces all existing queries with the newly supplied url decoded queries.
+ *

relationship to JAXRS 2.0

Like {@code WebTarget.queries}, except the + * values can be templatized.
ex.
*
    * template.queries(ImmutableMultimap.of("Signature", "{signature}"));
    * 
* - * @param queries if null, remove all queries. else value to replace all queries - * with. + * @param queries if null, remove all queries. else value to replace all queries with. * @see #queries() */ public RequestTemplate queries(Map> queries) { if (queries == null || queries.isEmpty()) { this.queries.clear(); } else { - for (Entry> entry : queries.entrySet()) + for (Entry> entry : queries.entrySet()) { query(entry.getKey(), toArray(entry.getValue(), String.class)); + } } return this; } @@ -315,26 +342,18 @@ public Map> queries() { } /** - * Replaces headers with the specified {@code configKey} with the - * {@code values} supplied. - *
- * When the {@code value} is {@code null}, all headers with the {@code configKey} - * are removed. - *
- *

relationship to JAXRS 2.0
- *
- * Like {@code WebTarget.queries} and {@code javax.ws.rs.client.Invocation.Builder.header}, - * except the values can be templatized. - *
- * ex. - *
+ * Replaces headers with the specified {@code configKey} with the {@code values} supplied.
+ * When the {@code value} is {@code null}, all headers with the {@code configKey} are removed. + *


relationship to JAXRS 2.0

Like {@code WebTarget.queries} and + * {@code javax.ws.rs.client.Invocation.Builder.header}, except the values can be templatized. + *
ex.
*
    * template.query("X-Application-Version", "{version}");
    * 
* - * @param name the name of the header - * @param values can be a single null to imply removing all values. Else no - * values are expected to be null. + * @param name the name of the header + * @param values can be a single null to imply removing all values. Else no values are expected to + * be null. * @see #headers() */ public RequestTemplate header(String name, String... values) { @@ -351,34 +370,30 @@ public RequestTemplate header(String name, String... values) { /* @see #header(String, String...) */ public RequestTemplate header(String name, Iterable values) { - if (values != null) + if (values != null) { return header(name, toArray(values, String.class)); + } return header(name, (String[]) null); } /** - * Replaces all existing headers with the newly supplied headers. - *
- *

relationship to JAXRS 2.0
- *
- * Like {@code Invocation.Builder.headers(MultivaluedMap)}, except the - * values can be templatized. - *
- * ex. - *
+ * Replaces all existing headers with the newly supplied headers.


relationship to + * JAXRS 2.0

Like {@code Invocation.Builder.headers(MultivaluedMap)}, except the + * values can be templatized.
ex.
*
-   * template.headers(ImmutableMultimap.of("X-Application-Version", "{version}"));
+   * template.headers(ImmutableMultimap.of("X-Application-Version",
+   * "{version}"));
    * 
* - * @param headers if null, remove all headers. else value to replace all headers - * with. + * @param headers if null, remove all headers. else value to replace all headers with. * @see #headers() */ public RequestTemplate headers(Map> headers) { - if (headers == null || headers.isEmpty()) + if (headers == null || headers.isEmpty()) { this.headers.clear(); - else + } else { this.headers.putAll(headers); + } return this; } @@ -392,9 +407,8 @@ public Map> headers() { } /** - * replaces the {@link feign.Util#CONTENT_LENGTH} header. - *
- * Usually populated by an {@link feign.codec.Encoder}. + * replaces the {@link feign.Util#CONTENT_LENGTH} header.
Usually populated by an {@link + * feign.codec.Encoder}. * * @see Request#body() */ @@ -408,9 +422,8 @@ public RequestTemplate body(byte[] bodyData, Charset charset) { } /** - * replaces the {@link feign.Util#CONTENT_LENGTH} header. - *
- * Usually populated by an {@link feign.codec.Encoder}. + * replaces the {@link feign.Util#CONTENT_LENGTH} header.
Usually populated by an {@link + * feign.codec.Encoder}. * * @see Request#body() */ @@ -420,8 +433,9 @@ public RequestTemplate body(String bodyText) { } /** - * The character set with which the body is encoded, or null if unknown or not applicable. When this is - * present, you can use {@code new String(req.body(), req.charset())} to access the body as a String. + * The character set with which the body is encoded, or null if unknown or not applicable. When + * this is present, you can use {@code new String(req.body(), req.charset())} to access the body + * as a String. */ public Charset charset() { return charset; @@ -468,84 +482,47 @@ private StringBuilder pullAnyQueriesOutOfUrl(StringBuilder url) { queries.clear(); } //Since we decode all queries, we want to use the - //query()-method to re-add them to ensure that all - //logic (such as url-encoding) are executed, giving - //a valid queryLine() - for(String key : firstQueries.keySet()) { - Collection values = firstQueries.get(key); - if(allValuesAreNull(values)) { - //Queryies where all values are null will - //be ignored by the query(key, value)-method - //So we manually avoid this case here, to ensure that - //we still fulfill the contract (ex. parameters without values) - queries.put(urlEncode(key), values); - } - else { - query(key, values); - } - - } + //query()-method to re-add them to ensure that all + //logic (such as url-encoding) are executed, giving + //a valid queryLine() + for (String key : firstQueries.keySet()) { + Collection values = firstQueries.get(key); + if (allValuesAreNull(values)) { + //Queryies where all values are null will + //be ignored by the query(key, value)-method + //So we manually avoid this case here, to ensure that + //we still fulfill the contract (ex. parameters without values) + queries.put(urlEncode(key), values); + } else { + query(key, values); + } + + } return new StringBuilder(url.substring(0, queryIndex)); } return url; } - private boolean allValuesAreNull(Collection values) { - if(values.isEmpty()) return true; - for(String val : values) { - if(val != null) return false; - } - return true; - } - - private static Map> parseAndDecodeQueries(String queryLine) { - Map> map = new LinkedHashMap>(); - if (emptyToNull(queryLine) == null) - return map; - if (queryLine.indexOf('&') == -1) { - if (queryLine.indexOf('=') != -1) - putKV(queryLine, map); - else - map.put(queryLine, null); - } else { - char[] chars = queryLine.toCharArray(); - int start = 0; - int i = 0; - for (; i < chars.length; i++) { - if (chars[i] == '&') { - putKV(queryLine.substring(start, i), map); - start = i + 1; - } - } - putKV(queryLine.substring(start, i), map); + private boolean allValuesAreNull(Collection values) { + if (values.isEmpty()) { + return true; } - return map; - } - - private static void putKV(String stringToParse, Map> map) { - String key; - String value; - // note that '=' can be a valid part of the value - int firstEq = stringToParse.indexOf('='); - if (firstEq == -1) { - key = urlDecode(stringToParse); - value = null; - } else { - key = urlDecode(stringToParse.substring(0, firstEq)); - value = urlDecode(stringToParse.substring(firstEq + 1)); + for (String val : values) { + if (val != null) { + return false; + } } - Collection values = map.containsKey(key) ? map.get(key) : new ArrayList(); - values.add(value); - map.put(key, values); + return true; } - @Override public String toString() { + @Override + public String toString() { return request().toString(); } /** - * Replaces query values which are templated with corresponding values from the {@code unencoded} map. - * Any unresolved queries are removed. + * Replaces query values which are templated with corresponding values from the {@code unencoded} + * map. Any unresolved queries are removed. */ public void replaceQueryValues(Map unencoded) { Iterator>> iterator = queries.entrySet().iterator(); @@ -582,8 +559,9 @@ public void replaceQueryValues(Map unencoded) { } public String queryLine() { - if (queries.isEmpty()) + if (queries.isEmpty()) { return ""; + } StringBuilder queryBuilder = new StringBuilder(); for (String field : queries.keySet()) { for (String value : valuesOrEmpty(queries, field)) { @@ -591,8 +569,9 @@ public String queryLine() { queryBuilder.append(field); if (value != null) { queryBuilder.append('='); - if (!value.isEmpty()) + if (!value.isEmpty()) { queryBuilder.append(value); + } } } } @@ -600,5 +579,11 @@ public String queryLine() { return queryBuilder.insert(0, '?').toString(); } - private static final long serialVersionUID = 1L; + interface Factory { + + /** + * create a request template using args passed to a method invocation. + */ + RequestTemplate create(Object[] argv); + } } diff --git a/core/src/main/java/feign/Response.java b/core/src/main/java/feign/Response.java index 4ea1941d13..b4b639dc47 100644 --- a/core/src/main/java/feign/Response.java +++ b/core/src/main/java/feign/Response.java @@ -28,21 +28,33 @@ import java.util.Map; import static feign.Util.UTF_8; -import static feign.Util.decodeOrDefault; import static feign.Util.checkNotNull; import static feign.Util.checkState; +import static feign.Util.decodeOrDefault; import static feign.Util.valuesOrEmpty; /** - * An immutable response to an http invocation which only returns string - * content. + * An immutable response to an http invocation which only returns string content. */ public final class Response { + private final int status; private final String reason; private final Map> headers; private final Body body; + private Response(int status, String reason, Map> headers, Body body) { + checkState(status >= 200, "Invalid status code: %s", status); + this.status = status; + this.reason = checkNotNull(reason, "reason"); + LinkedHashMap> + copyOf = + new LinkedHashMap>(); + copyOf.putAll(checkNotNull(headers, "headers")); + this.headers = Collections.unmodifiableMap(copyOf); + this.body = body; //nullable + } + public static Response create(int status, String reason, Map> headers, InputStream inputStream, Integer length) { return new Response(status, reason, headers, InputStreamBody.orNull(inputStream, length)); @@ -58,20 +70,11 @@ public static Response create(int status, String reason, Map> headers, Body body) { + public static Response create(int status, String reason, Map> headers, + Body body) { return new Response(status, reason, headers, body); } - private Response(int status, String reason, Map> headers, Body body) { - checkState(status >= 200, "Invalid status code: %s", status); - this.status = status; - this.reason = checkNotNull(reason, "reason"); - LinkedHashMap> copyOf = new LinkedHashMap>(); - copyOf.putAll(checkNotNull(headers, "headers")); - this.headers = Collections.unmodifiableMap(copyOf); - this.body = body; //nullable - } - /** * status code. ex {@code 200} * @@ -96,14 +99,27 @@ public Body body() { return body; } + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("HTTP/1.1 ").append(status).append(' ').append(reason).append('\n'); + for (String field : headers.keySet()) { + for (String value : valuesOrEmpty(headers, field)) { + builder.append(field).append(": ").append(value).append('\n'); + } + } + if (body != null) { + builder.append('\n').append(body); + } + return builder.toString(); + } + public interface Body extends Closeable { /** - * length in bytes, if known. Null if not. - *
- *

Note
This is an integer as most implementations cannot do - * bodies greater than 2GB. Moreover, the scope of this interface doesn't include - * large bodies. + * length in bytes, if known. Null if not.


Note
This is an integer as + * most implementations cannot do bodies greater than 2GB. Moreover, the scope of this interface + * doesn't include large bodies. */ Integer length(); @@ -124,43 +140,55 @@ public interface Body extends Closeable { } private static final class InputStreamBody implements Response.Body { - private static Body orNull(InputStream inputStream, Integer length) { - if (inputStream == null) { - return null; - } - return new InputStreamBody(inputStream, length); - } private final InputStream inputStream; private final Integer length; - private InputStreamBody(InputStream inputStream, Integer length) { this.inputStream = inputStream; this.length = length; } - @Override public Integer length() { + private static Body orNull(InputStream inputStream, Integer length) { + if (inputStream == null) { + return null; + } + return new InputStreamBody(inputStream, length); + } + + @Override + public Integer length() { return length; } - @Override public boolean isRepeatable() { + @Override + public boolean isRepeatable() { return false; } - @Override public InputStream asInputStream() throws IOException { + @Override + public InputStream asInputStream() throws IOException { return inputStream; } - @Override public Reader asReader() throws IOException { + @Override + public Reader asReader() throws IOException { return new InputStreamReader(inputStream, UTF_8); } - @Override public void close() throws IOException { + @Override + public void close() throws IOException { inputStream.close(); } } private static final class ByteArrayBody implements Response.Body { + + private final byte[] data; + + public ByteArrayBody(byte[] data) { + this.data = data; + } + private static Body orNull(byte[] data) { if (data == null) { return null; @@ -176,47 +204,33 @@ private static Body orNull(String text, Charset charset) { return new ByteArrayBody(text.getBytes(charset)); } - private final byte[] data; - - public ByteArrayBody(byte[] data) { - this.data = data; - } - - @Override public Integer length() { + @Override + public Integer length() { return data.length; } - @Override public boolean isRepeatable() { + @Override + public boolean isRepeatable() { return true; } - @Override public InputStream asInputStream() throws IOException { + @Override + public InputStream asInputStream() throws IOException { return new ByteArrayInputStream(data); } - @Override public Reader asReader() throws IOException { + @Override + public Reader asReader() throws IOException { return new InputStreamReader(asInputStream(), UTF_8); } - @Override public void close() throws IOException { + @Override + public void close() throws IOException { } - @Override public String toString() { + @Override + public String toString() { return decodeOrDefault(data, UTF_8, "Binary data"); } } - - @Override public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append("HTTP/1.1 ").append(status).append(' ').append(reason).append('\n'); - for (String field : headers.keySet()) { - for (String value : valuesOrEmpty(headers, field)) { - builder.append(field).append(": ").append(value).append('\n'); - } - } - if (body != null) { - builder.append('\n').append(body); - } - return builder.toString(); - } } diff --git a/core/src/main/java/feign/RetryableException.java b/core/src/main/java/feign/RetryableException.java index d812cbc1e3..ff91ba0db4 100644 --- a/core/src/main/java/feign/RetryableException.java +++ b/core/src/main/java/feign/RetryableException.java @@ -18,9 +18,8 @@ import java.util.Date; /** - * This exception is raised when the {@link Response} is deemed to be retryable, - * typically via an {@link feign.codec.ErrorDecoder} when the {@link Response#status() - * status} is 503. + * This exception is raised when the {@link Response} is deemed to be retryable, typically via an + * {@link feign.codec.ErrorDecoder} when the {@link Response#status() status} is 503. */ public class RetryableException extends FeignException { @@ -29,8 +28,7 @@ public class RetryableException extends FeignException { private final Long retryAfter; /** - * @param retryAfter usually corresponds to the {@link feign.Util#RETRY_AFTER} - * header. + * @param retryAfter usually corresponds to the {@link feign.Util#RETRY_AFTER} header. */ public RetryableException(String message, Throwable cause, Date retryAfter) { super(message, cause); @@ -38,8 +36,7 @@ public RetryableException(String message, Throwable cause, Date retryAfter) { } /** - * @param retryAfter usually corresponds to the {@link feign.Util#RETRY_AFTER} - * header. + * @param retryAfter usually corresponds to the {@link feign.Util#RETRY_AFTER} header. */ public RetryableException(String message, Date retryAfter) { super(message); @@ -47,9 +44,8 @@ public RetryableException(String message, Date retryAfter) { } /** - * Sometimes corresponds to the {@link feign.Util#RETRY_AFTER} header - * present in {@code 503} status. Other times parsed from an - * application-specific response. Null if unknown. + * Sometimes corresponds to the {@link feign.Util#RETRY_AFTER} header present in {@code 503} + * status. Other times parsed from an application-specific response. Null if unknown. */ public Date retryAfter() { return retryAfter != null ? new Date(retryAfter) : null; diff --git a/core/src/main/java/feign/Retryer.java b/core/src/main/java/feign/Retryer.java index b6cafe5db8..301dd7c8d4 100644 --- a/core/src/main/java/feign/Retryer.java +++ b/core/src/main/java/feign/Retryer.java @@ -19,14 +19,12 @@ /** * Created for each invocation to {@link Client#execute(Request, feign.Request.Options)}. - * Implementations may keep state to determine if retry operations should - * continue or not. + * Implementations may keep state to determine if retry operations should continue or not. */ public interface Retryer { /** - * if retry is permitted, return (possibly after sleeping). Otherwise - * propagate the exception. + * if retry is permitted, return (possibly after sleeping). Otherwise propagate the exception. */ void continueOrPropagate(RetryableException e); @@ -35,15 +33,8 @@ public static class Default implements Retryer { private final int maxAttempts; private final long period; private final long maxPeriod; - - // visible for testing; - protected long currentTimeMillis() { - return System.currentTimeMillis(); - } - int attempt; long sleptForMillis; - public Default() { this(100, SECONDS.toMillis(1), 5); } @@ -55,17 +46,25 @@ public Default(long period, long maxPeriod, int maxAttempts) { this.attempt = 1; } + // visible for testing; + protected long currentTimeMillis() { + return System.currentTimeMillis(); + } + public void continueOrPropagate(RetryableException e) { - if (attempt++ >= maxAttempts) + if (attempt++ >= maxAttempts) { throw e; + } long interval; if (e.retryAfter() != null) { interval = e.retryAfter().getTime() - currentTimeMillis(); - if (interval > maxPeriod) + if (interval > maxPeriod) { interval = maxPeriod; - if (interval < 0) + } + if (interval < 0) { return; + } } else { interval = nextMaxInterval(); } @@ -78,11 +77,9 @@ public void continueOrPropagate(RetryableException e) { } /** - * Calculates the time interval to a retry attempt. - *
- * The interval increases exponentially with each attempt, at a rate of - * nextInterval *= 1.5 (where 1.5 is the backoff factor), to the maximum - * interval. + * Calculates the time interval to a retry attempt.
The interval increases exponentially + * with each attempt, at a rate of nextInterval *= 1.5 (where 1.5 is the backoff factor), to the + * maximum interval. * * @return time in nanoseconds from now until the next attempt. */ diff --git a/core/src/main/java/feign/SynchronousMethodHandler.java b/core/src/main/java/feign/SynchronousMethodHandler.java index 764636a503..73c7a09fda 100644 --- a/core/src/main/java/feign/SynchronousMethodHandler.java +++ b/core/src/main/java/feign/SynchronousMethodHandler.java @@ -15,14 +15,15 @@ */ package feign; +import java.io.IOException; +import java.util.List; +import java.util.concurrent.TimeUnit; + import feign.InvocationHandlerFactory.MethodHandler; import feign.Request.Options; import feign.codec.DecodeException; import feign.codec.Decoder; import feign.codec.ErrorDecoder; -import java.io.IOException; -import java.util.List; -import java.util.concurrent.TimeUnit; import static feign.FeignException.errorExecuting; import static feign.FeignException.errorReading; @@ -31,30 +32,6 @@ final class SynchronousMethodHandler implements MethodHandler { - static class Factory { - - private final Client client; - private final Retryer retryer; - private final List requestInterceptors; - private final Logger logger; - private final Logger.Level logLevel; - - Factory(Client client, Retryer retryer, List requestInterceptors, - Logger logger, Logger.Level logLevel) { - this.client = checkNotNull(client, "client"); - this.retryer = checkNotNull(retryer, "retryer"); - this.requestInterceptors = checkNotNull(requestInterceptors, "requestInterceptors"); - this.logger = checkNotNull(logger, "logger"); - this.logLevel = checkNotNull(logLevel, "logLevel"); - } - - public MethodHandler create(Target target, MethodMetadata md, RequestTemplate.Factory buildTemplateFromArgs, - Options options, Decoder decoder, ErrorDecoder errorDecoder) { - return new SynchronousMethodHandler(target, client, retryer, requestInterceptors, logger, logLevel, md, - buildTemplateFromArgs, options, decoder, errorDecoder); - } - } - private final MethodMetadata metadata; private final Target target; private final Client client; @@ -66,7 +43,6 @@ public MethodHandler create(Target target, MethodMetadata md, RequestTemplate private final Options options; private final Decoder decoder; private final ErrorDecoder errorDecoder; - private SynchronousMethodHandler(Target target, Client client, Retryer retryer, List requestInterceptors, Logger logger, Logger.Level logLevel, MethodMetadata metadata, @@ -75,7 +51,8 @@ private SynchronousMethodHandler(Target target, Client client, Retryer retrye 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.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); @@ -85,7 +62,8 @@ private SynchronousMethodHandler(Target target, Client client, Retryer retrye this.decoder = checkNotNull(decoder, "decoder for %s", target); } - @Override public Object invoke(Object[] argv) throws Throwable { + @Override + public Object invoke(Object[] argv) throws Throwable { RequestTemplate template = buildTemplateFromArgs.create(argv); Retryer retryer = this.retryer; while (true) { @@ -122,7 +100,8 @@ Object executeAndDecode(RequestTemplate template) throws Throwable { try { if (logLevel != Logger.Level.NONE) { - response = logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime); + response = + logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime); } if (response.status() >= 200 && response.status() < 300) { if (Response.class == metadata.returnType()) { @@ -131,7 +110,8 @@ Object executeAndDecode(RequestTemplate template) throws Throwable { } // Ensure the response body is disconnected byte[] bodyData = Util.toByteArray(response.body().asInputStream()); - return Response.create(response.status(), response.reason(), response.headers(), bodyData); + return Response + .create(response.status(), response.reason(), response.headers(), bodyData); } else if (void.class == metadata.returnType()) { return null; } else { @@ -170,4 +150,30 @@ Object decode(Response response) throws Throwable { throw new DecodeException(e.getMessage(), e); } } + + static class Factory { + + private final Client client; + private final Retryer retryer; + private final List requestInterceptors; + private final Logger logger; + private final Logger.Level logLevel; + + Factory(Client client, Retryer retryer, List requestInterceptors, + Logger logger, Logger.Level logLevel) { + this.client = checkNotNull(client, "client"); + this.retryer = checkNotNull(retryer, "retryer"); + this.requestInterceptors = checkNotNull(requestInterceptors, "requestInterceptors"); + this.logger = checkNotNull(logger, "logger"); + this.logLevel = checkNotNull(logLevel, "logLevel"); + } + + public MethodHandler create(Target target, MethodMetadata md, + RequestTemplate.Factory buildTemplateFromArgs, + Options options, Decoder decoder, ErrorDecoder errorDecoder) { + return new SynchronousMethodHandler(target, client, retryer, requestInterceptors, logger, + logLevel, md, + buildTemplateFromArgs, options, decoder, errorDecoder); + } + } } diff --git a/core/src/main/java/feign/Target.java b/core/src/main/java/feign/Target.java index c161017b73..2c82067fbd 100644 --- a/core/src/main/java/feign/Target.java +++ b/core/src/main/java/feign/Target.java @@ -19,14 +19,14 @@ import static feign.Util.emptyToNull; /** - *

relationship to JAXRS 2.0
- *
- * Similar to {@code javax.ws.rs.client.WebTarget}, as it produces requests. - * However, {@link RequestTemplate} is a closer match to {@code WebTarget}. + *

relationship to JAXRS 2.0

Similar to {@code + * javax.ws.rs.client.WebTarget}, as it produces requests. However, {@link RequestTemplate} is a + * closer match to {@code WebTarget}. * * @param type of the interface this target applies to. */ public interface Target { + /* The type of the interface this target applies to. ex. {@code Route53}. */ Class type(); @@ -37,12 +37,8 @@ public interface Target { String url(); /** - * Targets a template to this target, adding the {@link #url() base url} and - * any target-specific headers or query parameters. - *
- *
- * For example: - *
+ * Targets a template to this target, adding the {@link #url() base url} and any target-specific + * headers or query parameters.

For example:
*
    * public Request apply(RequestTemplate input) {
    *     input.insert(0, url());
@@ -50,16 +46,14 @@ public interface Target {
    *     return input.asRequest();
    * }
    * 
- *
- *

relationship to JAXRS 2.0
- *
- * This call is similar to {@code javax.ws.rs.client.WebTarget.request()}, - * except that we expect transient, but necessary decoration to be applied - * on invocation. + *


relationship to JAXRS 2.0

This call is similar to {@code + * javax.ws.rs.client.WebTarget.request()}, except that we expect transient, but necessary + * decoration to be applied on invocation. */ public Request apply(RequestTemplate input); public static class HardCodedTarget implements Target { + private final Class type; private final String name; private final String url; @@ -74,36 +68,43 @@ public HardCodedTarget(Class type, String name, String url) { this.url = checkNotNull(emptyToNull(url), "url"); } - @Override public Class type() { + @Override + public Class type() { return type; } - @Override public String name() { + @Override + public String name() { return name; } - @Override public String url() { + @Override + public String url() { return url; } /* no authentication or other special activity. just insert the url. */ - @Override public Request apply(RequestTemplate input) { - if (input.url().indexOf("http") != 0) + @Override + public Request apply(RequestTemplate input) { + if (input.url().indexOf("http") != 0) { input.insert(0, url()); + } return input.request(); } - @Override public boolean equals(Object obj) { + @Override + public boolean equals(Object obj) { if (obj instanceof HardCodedTarget) { HardCodedTarget other = (HardCodedTarget) obj; return type.equals(other.type) - && name.equals(other.name) - && url.equals(other.url); + && name.equals(other.name) + && url.equals(other.url); } return false; } - @Override public int hashCode() { + @Override + public int hashCode() { int result = 17; result = 31 * result + type.hashCode(); result = 31 * result + name.hashCode(); @@ -111,15 +112,18 @@ public HardCodedTarget(Class type, String name, String url) { return result; } - @Override public String toString() { + @Override + public String toString() { if (name.equals(url)) { return "HardCodedTarget(type=" + type.getSimpleName() + ", url=" + url + ")"; } - return "HardCodedTarget(type=" + type.getSimpleName() + ", name=" + name + ", url=" + url + ")"; + return "HardCodedTarget(type=" + type.getSimpleName() + ", name=" + name + ", url=" + url + + ")"; } } public static final class EmptyTarget implements Target { + private final Class type; private final String name; @@ -127,7 +131,7 @@ public static final class EmptyTarget implements Target { this.type = checkNotNull(type, "type"); this.name = checkNotNull(emptyToNull(name), "name"); } - + public static EmptyTarget create(Class type) { return new EmptyTarget(type, "empty:" + type.getSimpleName()); } @@ -136,42 +140,50 @@ public static EmptyTarget create(Class type, String name) { return new EmptyTarget(type, name); } - @Override public Class type() { + @Override + public Class type() { return type; } - @Override public String name() { + @Override + public String name() { return name; } - @Override public String url() { + @Override + public String url() { throw new UnsupportedOperationException("Empty targets don't have URLs"); } - @Override public Request apply(RequestTemplate input) { + @Override + public Request apply(RequestTemplate input) { if (input.url().indexOf("http") != 0) { - throw new UnsupportedOperationException("Request with non-absolute URL not supported with empty target"); + throw new UnsupportedOperationException( + "Request with non-absolute URL not supported with empty target"); } return input.request(); } - @Override public boolean equals(Object obj) { + @Override + public boolean equals(Object obj) { if (obj instanceof EmptyTarget) { EmptyTarget other = (EmptyTarget) obj; return type.equals(other.type) - && name.equals(other.name); + && name.equals(other.name); } return false; } - @Override public int hashCode() { + @Override + public int hashCode() { int result = 17; result = 31 * result + type.hashCode(); result = 31 * result + name.hashCode(); return result; } - @Override public String toString() { + @Override + public String toString() { if (name.equals("empty:" + type.getSimpleName())) { return "EmptyTarget(type=" + type.getSimpleName() + ")"; } diff --git a/core/src/main/java/feign/Types.java b/core/src/main/java/feign/Types.java index 397557751a..ffab3b9c76 100644 --- a/core/src/main/java/feign/Types.java +++ b/core/src/main/java/feign/Types.java @@ -33,9 +33,14 @@ * @author Jesse Wilson */ final class Types { - /** Type literal for {@code Map}. */ + + /** + * Type literal for {@code Map}. + */ static final Type MAP_STRING_WILDCARD = new ParameterizedTypeImpl(null, Map.class, String.class, - new WildcardTypeImpl(new Type[] { Object.class }, new Type[] { })); + new WildcardTypeImpl( + new Type[]{Object.class}, + new Type[]{})); private static final Type[] EMPTY_TYPE_ARRAY = new Type[0]; @@ -54,7 +59,9 @@ static Class getRawType(Type type) { // I'm not exactly sure why getRawType() returns Type instead of Class. Neal isn't either but // suspects some pathological case related to nested classes exists. Type rawType = parameterizedType.getRawType(); - if (!(rawType instanceof Class)) throw new IllegalArgumentException(); + if (!(rawType instanceof Class)) { + throw new IllegalArgumentException(); + } return (Class) rawType; } else if (type instanceof GenericArrayType) { @@ -72,11 +79,14 @@ static Class getRawType(Type type) { } else { String className = type == null ? "null" : type.getClass().getName(); throw new IllegalArgumentException("Expected a Class, ParameterizedType, or " - + "GenericArrayType, but <" + type + "> is of type " + className); + + "GenericArrayType, but <" + type + "> is of type " + + className); } } - /** Returns true if {@code a} and {@code b} are equal. */ + /** + * Returns true if {@code a} and {@code b} are equal. + */ static boolean equals(Type a, Type b) { if (a == b) { return true; // Also handles (a == null && b == null). @@ -85,32 +95,40 @@ static boolean equals(Type a, Type b) { return a.equals(b); // Class already specifies equals(). } else if (a instanceof ParameterizedType) { - if (!(b instanceof ParameterizedType)) return false; + if (!(b instanceof ParameterizedType)) { + return false; + } ParameterizedType pa = (ParameterizedType) a; ParameterizedType pb = (ParameterizedType) b; return equal(pa.getOwnerType(), pb.getOwnerType()) - && pa.getRawType().equals(pb.getRawType()) - && Arrays.equals(pa.getActualTypeArguments(), pb.getActualTypeArguments()); + && pa.getRawType().equals(pb.getRawType()) + && Arrays.equals(pa.getActualTypeArguments(), pb.getActualTypeArguments()); } else if (a instanceof GenericArrayType) { - if (!(b instanceof GenericArrayType)) return false; + if (!(b instanceof GenericArrayType)) { + return false; + } GenericArrayType ga = (GenericArrayType) a; GenericArrayType gb = (GenericArrayType) b; return equals(ga.getGenericComponentType(), gb.getGenericComponentType()); } else if (a instanceof WildcardType) { - if (!(b instanceof WildcardType)) return false; + if (!(b instanceof WildcardType)) { + return false; + } WildcardType wa = (WildcardType) a; WildcardType wb = (WildcardType) b; return Arrays.equals(wa.getUpperBounds(), wb.getUpperBounds()) - && Arrays.equals(wa.getLowerBounds(), wb.getLowerBounds()); + && Arrays.equals(wa.getLowerBounds(), wb.getLowerBounds()); } else if (a instanceof TypeVariable) { - if (!(b instanceof TypeVariable)) return false; + if (!(b instanceof TypeVariable)) { + return false; + } TypeVariable va = (TypeVariable) a; TypeVariable vb = (TypeVariable) b; return va.getGenericDeclaration() == vb.getGenericDeclaration() - && va.getName().equals(vb.getName()); + && va.getName().equals(vb.getName()); } else { return false; // This isn't a type we support! @@ -123,7 +141,9 @@ static boolean equals(Type a, Type b) { * result when the supertype is {@code Collection.class} is {@code Collection}. */ static Type getGenericSupertype(Type context, Class rawType, Class toResolve) { - if (toResolve == rawType) return context; + if (toResolve == rawType) { + return context; + } // We skip searching through interfaces if unknown is an interface. if (toResolve.isInterface()) { @@ -156,7 +176,9 @@ static Type getGenericSupertype(Type context, Class rawType, Class toResol private static int indexOf(Object[] array, Object toFind) { for (int i = 0; i < array.length; i++) { - if (toFind.equals(array[i])) return i; + if (toFind.equals(array[i])) { + return i; + } } throw new NoSuchElementException(); } @@ -181,9 +203,11 @@ static String typeToString(Type type) { * @param supertype a superclass of, or interface implemented by, this. */ static Type getSupertype(Type context, Class contextRawType, Class supertype) { - if (!supertype.isAssignableFrom(contextRawType)) throw new IllegalArgumentException(); + if (!supertype.isAssignableFrom(contextRawType)) { + throw new IllegalArgumentException(); + } return resolve(context, contextRawType, - getGenericSupertype(context, contextRawType, supertype)); + getGenericSupertype(context, contextRawType, supertype)); } static Type resolve(Type context, Class contextRawType, Type toResolve) { @@ -229,8 +253,8 @@ static Type resolve(Type context, Class contextRawType, Type toResolve) { } return changed - ? new ParameterizedTypeImpl(newOwnerType, original.getRawType(), args) - : original; + ? new ParameterizedTypeImpl(newOwnerType, original.getRawType(), args) + : original; } else if (toResolve instanceof WildcardType) { WildcardType original = (WildcardType) toResolve; @@ -240,12 +264,12 @@ static Type resolve(Type context, Class contextRawType, Type toResolve) { if (originalLowerBound.length == 1) { Type lowerBound = resolve(context, contextRawType, originalLowerBound[0]); if (lowerBound != originalLowerBound[0]) { - return new WildcardTypeImpl(new Type[] { Object.class }, new Type[] { lowerBound }); + return new WildcardTypeImpl(new Type[]{Object.class}, new Type[]{lowerBound}); } } else if (originalUpperBound.length == 1) { Type upperBound = resolve(context, contextRawType, originalUpperBound[0]); if (upperBound != originalUpperBound[0]) { - return new WildcardTypeImpl(new Type[] { upperBound }, EMPTY_TYPE_ARRAY); + return new WildcardTypeImpl(new Type[]{upperBound}, EMPTY_TYPE_ARRAY); } } return original; @@ -261,7 +285,9 @@ private static Type resolveTypeVariable( Class declaredByRaw = declaringClassOf(unknown); // We can't reduce this further. - if (declaredByRaw == null) return unknown; + if (declaredByRaw == null) { + return unknown; + } Type declaredBy = getGenericSupertype(context, contextRawType, declaredByRaw); if (declaredBy instanceof ParameterizedType) { @@ -288,6 +314,7 @@ private static void checkNotPrimitive(Type type) { } private static final class ParameterizedTypeImpl implements ParameterizedType { + private final Type ownerType; private final Type rawType; private final Type[] typeArguments; @@ -304,7 +331,9 @@ private static final class ParameterizedTypeImpl implements ParameterizedType { this.typeArguments = typeArguments.clone(); for (Type typeArgument : this.typeArguments) { - if (typeArgument == null) throw new NullPointerException(); + if (typeArgument == null) { + throw new NullPointerException(); + } checkNotPrimitive(typeArgument); } } @@ -321,18 +350,23 @@ public Type getOwnerType() { return ownerType; } - @Override public boolean equals(Object other) { + @Override + public boolean equals(Object other) { return other instanceof ParameterizedType && Types.equals(this, (ParameterizedType) other); } - @Override public int hashCode() { + @Override + public int hashCode() { return Arrays.hashCode(typeArguments) ^ rawType.hashCode() ^ hashCodeOrZero(ownerType); } - @Override public String toString() { + @Override + public String toString() { StringBuilder result = new StringBuilder(30 * (typeArguments.length + 1)); result.append(typeToString(rawType)); - if (typeArguments.length == 0) return result.toString(); + if (typeArguments.length == 0) { + return result.toString(); + } result.append("<").append(typeToString(typeArguments[0])); for (int i = 1; i < typeArguments.length; i++) { result.append(", ").append(typeToString(typeArguments[i])); @@ -342,6 +376,7 @@ public Type getOwnerType() { } private static final class GenericArrayTypeImpl implements GenericArrayType { + private final Type componentType; GenericArrayTypeImpl(Type componentType) { @@ -352,41 +387,55 @@ public Type getGenericComponentType() { return componentType; } - @Override public boolean equals(Object o) { + @Override + public boolean equals(Object o) { return o instanceof GenericArrayType - && Types.equals(this, (GenericArrayType) o); + && Types.equals(this, (GenericArrayType) o); } - @Override public int hashCode() { + @Override + public int hashCode() { return componentType.hashCode(); } - @Override public String toString() { + @Override + public String toString() { return typeToString(componentType) + "[]"; } } /** - * The WildcardType interface supports multiple upper bounds and multiple - * lower bounds. We only support what the Java 6 language needs - at most one - * bound. If a lower bound is set, the upper bound must be Object.class. + * The WildcardType interface supports multiple upper bounds and multiple lower bounds. We only + * support what the Java 6 language needs - at most one bound. If a lower bound is set, the upper + * bound must be Object.class. */ private static final class WildcardTypeImpl implements WildcardType { + private final Type upperBound; private final Type lowerBound; WildcardTypeImpl(Type[] upperBounds, Type[] lowerBounds) { - if (lowerBounds.length > 1) throw new IllegalArgumentException(); - if (upperBounds.length != 1) throw new IllegalArgumentException(); + if (lowerBounds.length > 1) { + throw new IllegalArgumentException(); + } + if (upperBounds.length != 1) { + throw new IllegalArgumentException(); + } if (lowerBounds.length == 1) { - if (lowerBounds[0] == null) throw new NullPointerException(); + if (lowerBounds[0] == null) { + throw new NullPointerException(); + } checkNotPrimitive(lowerBounds[0]); - if (upperBounds[0] != Object.class) throw new IllegalArgumentException(); + if (upperBounds[0] != Object.class) { + throw new IllegalArgumentException(); + } this.lowerBound = lowerBounds[0]; this.upperBound = Object.class; } else { - if (upperBounds[0] == null) throw new NullPointerException(); + if (upperBounds[0] == null) { + throw new NullPointerException(); + } checkNotPrimitive(upperBounds[0]); this.lowerBound = null; this.upperBound = upperBounds[0]; @@ -394,25 +443,32 @@ private static final class WildcardTypeImpl implements WildcardType { } public Type[] getUpperBounds() { - return new Type[] { upperBound }; + return new Type[]{upperBound}; } public Type[] getLowerBounds() { - return lowerBound != null ? new Type[] { lowerBound } : EMPTY_TYPE_ARRAY; + return lowerBound != null ? new Type[]{lowerBound} : EMPTY_TYPE_ARRAY; } - @Override public boolean equals(Object other) { + @Override + public boolean equals(Object other) { return other instanceof WildcardType && Types.equals(this, (WildcardType) other); } - @Override public int hashCode() { + @Override + public int hashCode() { // This equals Arrays.hashCode(getLowerBounds()) ^ Arrays.hashCode(getUpperBounds()). return (lowerBound != null ? 31 + lowerBound.hashCode() : 1) ^ (31 + upperBound.hashCode()); } - @Override public String toString() { - if (lowerBound != null) return "? super " + typeToString(lowerBound); - if (upperBound == Object.class) return "?"; + @Override + public String toString() { + if (lowerBound != null) { + return "? super " + typeToString(lowerBound); + } + if (upperBound == Object.class) { + return "?"; + } return "? extends " + typeToString(upperBound); } } diff --git a/core/src/main/java/feign/Util.java b/core/src/main/java/feign/Util.java index 2b847fa6c6..7469c9b03f 100644 --- a/core/src/main/java/feign/Util.java +++ b/core/src/main/java/feign/Util.java @@ -40,8 +40,6 @@ * Utilities, typically copied in from guava, so as to avoid dependency conflicts. */ public class Util { - private Util() { // no instances - } /** * The HTTP Content-Length header field name. @@ -59,16 +57,20 @@ private Util() { // no instances * 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 /** * UTF-8: eight-bit UCS Transformation Format. */ public static final Charset UTF_8 = Charset.forName("UTF-8"); + + // com.google.common.base.Charsets /** * ISO-8859-1: ISO Latin Alphabet Number 1 (ISO-LATIN-1). */ public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1"); + private static final int BUF_SIZE = 0x800; // 2K chars (4K bytes) + + private Util() { // no instances + } /** * Copy of {@code com.google.common.base.Preconditions#checkArgument}. @@ -150,21 +152,24 @@ public static void ensureClosed(Closeable closeable) { } /** - * Resolves the last type parameter of the parameterized {@code supertype}, based on the {@code genericContext}, - * into its upper bounds. - *

- * Implementation copied from {@code retrofit.RestMethodInfo}. + * Resolves the last type parameter of the parameterized {@code supertype}, based on the {@code + * genericContext}, into its upper bounds.

Implementation copied from {@code + * retrofit.RestMethodInfo}. * * @param genericContext Ex. {@link java.lang.reflect.Field#getGenericType()} * @param supertype Ex. {@code Decoder.class} * @return in the example above, the type parameter of {@code Decoder}. - * @throws IllegalStateException if {@code supertype} cannot be resolved into a parameterized type using - * {@code context}. - */ - public static Type resolveLastTypeParameter(Type genericContext, Class supertype) throws IllegalStateException { - Type resolvedSuperType = Types.getSupertype(genericContext, Types.getRawType(genericContext), supertype); - checkState(resolvedSuperType instanceof ParameterizedType, "could not resolve %s into a parameterized type %s", - genericContext, supertype); + * @throws IllegalStateException if {@code supertype} cannot be resolved into a parameterized type + * using {@code context}. + */ + public static Type resolveLastTypeParameter(Type genericContext, Class supertype) + throws IllegalStateException { + Type + resolvedSuperType = + Types.getSupertype(genericContext, Types.getRawType(genericContext), supertype); + checkState(resolvedSuperType instanceof ParameterizedType, + "could not resolve %s into a parameterized type %s", + genericContext, supertype); Type[] types = ParameterizedType.class.cast(resolvedSuperType).getActualTypeArguments(); for (int i = 0; i < types.length; i++) { Type type = types[i]; @@ -175,8 +180,6 @@ public static Type resolveLastTypeParameter(Type genericContext, Class supert return types[types.length - 1]; } - private static final int BUF_SIZE = 0x800; // 2K chars (4K bytes) - /** * Adapted from {@code com.google.common.io.CharStreams.toString()}. */ diff --git a/core/src/main/java/feign/auth/Base64.java b/core/src/main/java/feign/auth/Base64.java index f75c092faf..c565bc7c84 100644 --- a/core/src/main/java/feign/auth/Base64.java +++ b/core/src/main/java/feign/auth/Base64.java @@ -19,11 +19,18 @@ /** * copied from okhttp + * * @author Alexander Y. Kleymenov */ final class Base64 { public static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + private static final byte[] MAP = new byte[]{ + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', + 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', + 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', + '5', '6', '7', '8', '9', '+', '/' + }; private Base64() { } @@ -119,13 +126,6 @@ public static byte[] decode(byte[] in, int len) { return result; } - private static final byte[] MAP = new byte[] { - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', - 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', - 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', - '5', '6', '7', '8', '9', '+', '/' - }; - public static String encode(byte[] in) { int length = (in.length + 2) * 4 / 3; byte[] out = new byte[length]; diff --git a/core/src/main/java/feign/auth/BasicAuthRequestInterceptor.java b/core/src/main/java/feign/auth/BasicAuthRequestInterceptor.java index 318f36f117..7539e7620d 100644 --- a/core/src/main/java/feign/auth/BasicAuthRequestInterceptor.java +++ b/core/src/main/java/feign/auth/BasicAuthRequestInterceptor.java @@ -15,23 +15,24 @@ */ package feign.auth; +import java.nio.charset.Charset; + import feign.RequestInterceptor; import feign.RequestTemplate; -import java.nio.charset.Charset; - -import static feign.Util.checkNotNull; import static feign.Util.ISO_8859_1; +import static feign.Util.checkNotNull; /** * An interceptor that adds the request header needed to use HTTP basic authentication. */ public class BasicAuthRequestInterceptor implements RequestInterceptor { + private final String headerValue; /** - * Creates an interceptor that authenticates all requests with the specified username and password encoded using - * ISO-8859-1. + * Creates an interceptor that authenticates all requests with the specified username and password + * encoded using ISO-8859-1. * * @param username the username to use for authentication * @param password the password to use for authentication @@ -41,12 +42,12 @@ public BasicAuthRequestInterceptor(String username, String password) { } /** - * Creates an interceptor that authenticates all requests with the specified username and password encoded using - * the specified charset. + * Creates an interceptor that authenticates all requests with the specified username and password + * encoded using the specified charset. * * @param username the username to use for authentication * @param password the password to use for authentication - * @param charset the charset to use when encoding the credentials + * @param charset the charset to use when encoding the credentials */ public BasicAuthRequestInterceptor(String username, String password, Charset charset) { checkNotNull(username, "username"); @@ -54,10 +55,6 @@ public BasicAuthRequestInterceptor(String username, String password, Charset cha this.headerValue = "Basic " + base64Encode((username + ":" + password).getBytes(charset)); } - @Override public void apply(RequestTemplate template) { - template.header("Authorization", headerValue); - } - /* * This uses a Sun internal method; if we ever encounter a case where this method is not available, the appropriate * response would be to pull the necessary portions of Guava's BaseEncoding class into Util. @@ -65,5 +62,10 @@ public BasicAuthRequestInterceptor(String username, String password, Charset cha private static String base64Encode(byte[] bytes) { return Base64.encode(bytes); } + + @Override + public void apply(RequestTemplate template) { + template.header("Authorization", headerValue); + } } diff --git a/core/src/main/java/feign/codec/DecodeException.java b/core/src/main/java/feign/codec/DecodeException.java index 1671bbdb60..720884b0c1 100644 --- a/core/src/main/java/feign/codec/DecodeException.java +++ b/core/src/main/java/feign/codec/DecodeException.java @@ -20,12 +20,14 @@ import static feign.Util.checkNotNull; /** - * Similar to {@code javax.websocket.DecodeException}, raised when a problem - * occurs decoding a message. Note that {@code DecodeException} is not an - * {@code IOException}, nor does it have one set as its cause. + * Similar to {@code javax.websocket.DecodeException}, raised when a problem occurs decoding a + * message. Note that {@code DecodeException} is not an {@code IOException}, nor does it have one + * set as its cause. */ public class DecodeException extends FeignException { + private static final long serialVersionUID = 1L; + /** * @param message the reason for the failure. */ @@ -40,6 +42,4 @@ public DecodeException(String message) { public DecodeException(String message, Throwable cause) { super(checkNotNull(message, "message"), checkNotNull(cause, "cause")); } - - private static final long serialVersionUID = 1L; } diff --git a/core/src/main/java/feign/codec/Decoder.java b/core/src/main/java/feign/codec/Decoder.java index 346b149bfb..58502afb6e 100644 --- a/core/src/main/java/feign/codec/Decoder.java +++ b/core/src/main/java/feign/codec/Decoder.java @@ -15,20 +15,17 @@ */ package feign.codec; +import java.io.IOException; +import java.lang.reflect.Type; + import feign.FeignException; import feign.Response; import feign.Util; -import java.io.IOException; -import java.lang.reflect.Type; - /** - * Decodes an HTTP response into a single object of the given {@code type}. Invoked when - * {@link Response#status()} is in the 2xx range and the return type is neither {@code void} nor {@code Response}. - *

- *

- * Example Implementation:
- *

+ * Decodes an HTTP response into a single object of the given {@code type}. Invoked when {@link + * Response#status()} is in the 2xx range and the return type is neither {@code void} nor {@code + * Response}.

Example Implementation:

*

  * public class GsonDecoder implements Decoder {
  *   private final Gson gson = new Gson();
@@ -47,25 +44,22 @@
  *   }
  * }
  * 
- *
- *

Implementation Note

- * The {@code type} parameter will correspond to the - * {@link java.lang.reflect.Method#getGenericReturnType() generic return type} - * of an {@link feign.Target#type() interface} processed by - * {@link feign.Feign#newInstance(feign.Target)}. When writing your - * implementation of Decoder, ensure you also test parameterized types such as - * {@code List}. - * + *

Implementation Note

The {@code type} parameter will correspond to the {@link + * java.lang.reflect.Method#getGenericReturnType() generic return type} of an {@link + * feign.Target#type() interface} processed by {@link feign.Feign#newInstance(feign.Target)}. When + * writing your implementation of Decoder, ensure you also test parameterized types such as {@code + * List}. */ public interface Decoder { + /** - * Decodes an http response into an object corresponding to its - * {@link java.lang.reflect.Method#getGenericReturnType() generic return type}. - * If you need to wrap exceptions, please do so via {@link DecodeException}. + * Decodes an http response into an object corresponding to its {@link + * java.lang.reflect.Method#getGenericReturnType() generic return type}. If you need to wrap + * exceptions, please do so via {@link DecodeException}. * * @param response the response to decode - * @param type {@link java.lang.reflect.Method#getGenericReturnType() generic return type} - * of the method corresponding to this {@code response}. + * @param type {@link java.lang.reflect.Method#getGenericReturnType() generic return type} of + * the method corresponding to this {@code response}. * @return instance of {@code type} * @throws IOException will be propagated safely to the caller. * @throws DecodeException when decoding failed due to a checked exception besides IOException. @@ -77,6 +71,7 @@ public interface Decoder { * Default implementation of {@code Decoder}. */ public class Default extends StringDecoder { + @Override public Object decode(Response response, Type type) throws IOException { Response.Body body = response.body(); diff --git a/core/src/main/java/feign/codec/EncodeException.java b/core/src/main/java/feign/codec/EncodeException.java index bc9c660ca0..e481c2795b 100644 --- a/core/src/main/java/feign/codec/EncodeException.java +++ b/core/src/main/java/feign/codec/EncodeException.java @@ -20,12 +20,14 @@ import static feign.Util.checkNotNull; /** - * Similar to {@code javax.websocket.EncodeException}, raised when a problem - * occurs encoding a message. Note that {@code EncodeException} is not an - * {@code IOException}, nor does it have one set as its cause. + * Similar to {@code javax.websocket.EncodeException}, raised when a problem occurs encoding a + * message. Note that {@code EncodeException} is not an {@code IOException}, nor does it have one + * set as its cause. */ public class EncodeException extends FeignException { + private static final long serialVersionUID = 1L; + /** * @param message the reason for the failure. */ @@ -40,6 +42,4 @@ public EncodeException(String message) { public EncodeException(String message, Throwable cause) { super(checkNotNull(message, "message"), checkNotNull(cause, "cause")); } - - private static final long serialVersionUID = 1L; } diff --git a/core/src/main/java/feign/codec/Encoder.java b/core/src/main/java/feign/codec/Encoder.java index b34c55242c..a49afbbf61 100644 --- a/core/src/main/java/feign/codec/Encoder.java +++ b/core/src/main/java/feign/codec/Encoder.java @@ -15,23 +15,22 @@ */ package feign.codec; -import feign.RequestTemplate; import java.lang.reflect.Type; +import feign.RequestTemplate; + import static java.lang.String.format; /** - * Encodes an object into an HTTP request body. Like {@code javax.websocket.Encoder}. - * {@code Encoder} is used when a method parameter has no {@code @Param} annotation. - * For example:
+ * Encodes an object into an HTTP request body. Like {@code javax.websocket.Encoder}. {@code + * Encoder} is used when a method parameter has no {@code @Param} annotation. For example:
*

*

  * @POST
  * @Path("/")
  * void create(User user);
  * 
- * Example implementation:
- *

+ * Example implementation:

*

  * public class GsonEncoder implements Encoder {
  *   private final Gson gson;
@@ -47,16 +46,14 @@
  * }
  * 
* - *

- *

Form encoding

- *
- * If any parameters are found in {@link feign.MethodMetadata#formParams()}, they will be - * collected and passed to the Encoder as a {@code Map}. - *
+ *

Form encoding


If any parameters are found in {@link + * feign.MethodMetadata#formParams()}, they will be collected and passed to the Encoder as a {@code + * Map}.
*
  * @POST
  * @Path("/")
- * Session login(@Param("username") String username, @Param("password") String password);
+ * Session login(@Param("username") String username, @Param("password") String
+ * password);
  * 
*/ public interface Encoder { @@ -64,8 +61,9 @@ public interface Encoder { /** * Converts objects to an appropriate representation in the template. * - * @param object what to encode as the request body. - * @param bodyType the type the object should be encoded as. {@code Map}, if form encoding. + * @param object what to encode as the request body. + * @param bodyType the type the object should be encoded as. {@code Map}, if form + * encoding. * @param template the request template to populate. * @throws EncodeException when encoding failed due to a checked exception. */ @@ -75,13 +73,16 @@ public interface Encoder { * Default implementation of {@code Encoder}. */ class Default implements Encoder { - @Override public void encode(Object object, Type bodyType, RequestTemplate template) { + + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) { if (bodyType == String.class) { template.body(object.toString()); } else if (bodyType == byte[].class) { template.body((byte[]) object, null); } else if (object != null) { - throw new EncodeException(format("%s is not a type supported by this encoder.", object.getClass())); + throw new EncodeException( + format("%s is not a type supported by this encoder.", object.getClass())); } } } diff --git a/core/src/main/java/feign/codec/ErrorDecoder.java b/core/src/main/java/feign/codec/ErrorDecoder.java index 273202d400..3977e39884 100644 --- a/core/src/main/java/feign/codec/ErrorDecoder.java +++ b/core/src/main/java/feign/codec/ErrorDecoder.java @@ -35,10 +35,7 @@ /** * Allows you to massage an exception into a application-specific one. Converting out to a throttle - * exception are examples of this in use. - *
- * Ex. - *
+ * exception are examples of this in use.
Ex.
*
  * class IllegalArgumentExceptionOn404Decoder extends ErrorDecoder {
  *
@@ -51,30 +48,27 @@
  *
  * }
  * 
- *
- * Error handling
- *
- * Responses where {@link Response#status()} is not in the 2xx range are - * classified as errors, addressed by the {@link ErrorDecoder}. That said, - * certain RPC apis return errors defined in the {@link Response#body()} even on - * a 200 status. For example, in the DynECT api, a job still running condition - * is returned with a 200 status, encoded in json. When scenarios like this - * occur, you should raise an application-specific exception (which may be + *
Error handling

Responses where {@link Response#status()} is not in the 2xx + * range are classified as errors, addressed by the {@link ErrorDecoder}. That said, certain RPC + * apis return errors defined in the {@link Response#body()} even on a 200 status. For example, in + * the DynECT api, a job still running condition is returned with a 200 status, encoded in json. + * When scenarios like this occur, you should raise an application-specific exception (which may be * {@link feign.RetryableException retryable}). */ public interface ErrorDecoder { /** - * Implement this method in order to decode an HTTP {@link Response} when - * {@link Response#status()} is not in the 2xx range. Please raise application-specific exceptions where possible. - * If your exception is retryable, wrap or subclass {@link RetryableException} + * Implement this method in order to decode an HTTP {@link Response} when {@link + * Response#status()} is not in the 2xx range. Please raise application-specific exceptions where + * possible. If your exception is retryable, wrap or subclass {@link RetryableException} * - * @param methodKey {@link feign.Feign#configKey} of the java method that invoked the request. ex. {@code IAM#getUser()} - * @param response HTTP response where {@link Response#status() status} is greater than or equal to {@code 300}. - * @return Exception IOException, if there was a network error reading the - * response or an application-specific exception decoded by the - * implementation. If the throwable is retryable, it should be - * wrapped, or a subtype of {@link RetryableException} + * @param methodKey {@link feign.Feign#configKey} of the java method that invoked the request. + * ex. {@code IAM#getUser()} + * @param response HTTP response where {@link Response#status() status} is greater than or equal + * to {@code 300}. + * @return Exception IOException, if there was a network error reading the response or an + * application-specific exception decoded by the implementation. If the throwable is retryable, it + * should be wrapped, or a subtype of {@link RetryableException} */ public Exception decode(String methodKey, Response response); @@ -86,8 +80,9 @@ public static class Default implements ErrorDecoder { public Exception decode(String methodKey, Response response) { FeignException exception = errorStatus(methodKey, response); Date retryAfter = retryAfterDecoder.apply(firstOrNull(response.headers(), RETRY_AFTER)); - if (retryAfter != null) + if (retryAfter != null) { return new RetryableException(exception.getMessage(), exception, retryAfter); + } return exception; } @@ -100,40 +95,38 @@ private T firstOrNull(Map> map, String key) { } /** - * Decodes a {@link feign.Util#RETRY_AFTER} header into an absolute date, - * if possible. - *
- * See Retry-After - * format + * Decodes a {@link feign.Util#RETRY_AFTER} header into an absolute date, if possible.
See Retry-After format */ static class RetryAfterDecoder { - static final DateFormat RFC822_FORMAT = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", US); + + static final DateFormat + RFC822_FORMAT = + new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", US); private final DateFormat rfc822Format; RetryAfterDecoder() { this(RFC822_FORMAT); } - protected long currentTimeNanos() { - return System.currentTimeMillis(); - } - RetryAfterDecoder(DateFormat rfc822Format) { this.rfc822Format = checkNotNull(rfc822Format, "rfc822Format"); } + protected long currentTimeNanos() { + return System.currentTimeMillis(); + } + /** - * returns a date that corresponds to the first time a request can be - * retried. + * returns a date that corresponds to the first time a request can be retried. * - * @param retryAfter String in Retry-After format */ public Date apply(String retryAfter) { - if (retryAfter == null) + if (retryAfter == null) { return null; + } if (retryAfter.matches("^[0-9]+$")) { long currentTimeMillis = NANOSECONDS.toMillis(currentTimeNanos()); long deltaMillis = SECONDS.toMillis(Long.parseLong(retryAfter)); diff --git a/core/src/main/java/feign/codec/StringDecoder.java b/core/src/main/java/feign/codec/StringDecoder.java index ae35eca978..261d0357f9 100644 --- a/core/src/main/java/feign/codec/StringDecoder.java +++ b/core/src/main/java/feign/codec/StringDecoder.java @@ -15,15 +15,16 @@ */ package feign.codec; -import feign.Response; -import feign.Util; - import java.io.IOException; import java.lang.reflect.Type; +import feign.Response; +import feign.Util; + import static java.lang.String.format; public class StringDecoder implements Decoder { + @Override public Object decode(Response response, Type type) throws IOException { Response.Body body = response.body(); diff --git a/core/src/test/java/feign/DefaultContractTest.java b/core/src/test/java/feign/DefaultContractTest.java index 12e7bba057..607244e6a9 100644 --- a/core/src/test/java/feign/DefaultContractTest.java +++ b/core/src/test/java/feign/DefaultContractTest.java @@ -16,114 +16,106 @@ package feign; import com.google.gson.reflect.TypeToken; -import java.net.URI; -import java.util.Date; -import java.util.List; + import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import java.net.URI; +import java.util.Date; +import java.util.List; + import static feign.assertj.FeignAssertions.assertThat; import static java.util.Arrays.asList; import static org.assertj.core.data.MapEntry.entry; /** * Tests interfaces defined per {@link Contract.Default} are interpreted into expected {@link feign - * .RequestTemplate template} - * instances. + * .RequestTemplate template} instances. */ public class DefaultContractTest { - @Rule public final ExpectedException thrown = ExpectedException.none(); - Contract.Default contract = new Contract.Default(); - - interface Methods { - @RequestLine("POST /") void post(); - - @RequestLine("PUT /") void put(); + @Rule + public final ExpectedException thrown = ExpectedException.none(); - @RequestLine("GET /") void get(); - - @RequestLine("DELETE /") void delete(); - } + Contract.Default contract = new Contract.Default(); - @Test public void httpMethods() throws Exception { - assertThat(contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("post")).template()) + @Test + public void httpMethods() throws Exception { + assertThat( + contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("post")).template()) .hasMethod("POST"); - assertThat(contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("put")).template()) + assertThat( + contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("put")).template()) .hasMethod("PUT"); - assertThat(contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("get")).template()) + assertThat( + contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("get")).template()) .hasMethod("GET"); - assertThat(contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("delete")).template()) + assertThat( + contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("delete")).template()) .hasMethod("DELETE"); } - interface BodyParams { - @RequestLine("POST") Response post(List body); - - @RequestLine("POST") Response tooMany(List body, List body2); - } - - @Test public void bodyParamIsGeneric() throws Exception { - MethodMetadata md = contract.parseAndValidatateMetadata(BodyParams.class.getDeclaredMethod("post", List.class)); + @Test + public void bodyParamIsGeneric() throws Exception { + MethodMetadata + md = + contract.parseAndValidatateMetadata(BodyParams.class.getDeclaredMethod("post", List.class)); assertThat(md.bodyIndex()) .isEqualTo(0); assertThat(md.bodyType()) - .isEqualTo(new TypeToken>(){}.getType()); + .isEqualTo(new TypeToken>() { + }.getType()); } - @Test public void tooManyBodies() throws Exception { + @Test + public void tooManyBodies() throws Exception { thrown.expect(IllegalStateException.class); thrown.expectMessage("Method has too many Body"); contract.parseAndValidatateMetadata( BodyParams.class.getDeclaredMethod("tooMany", List.class, List.class)); } - interface CustomMethod { - @RequestLine("PATCH") Response patch(); - } - - @Test public void customMethodWithoutPath() throws Exception { - assertThat(contract.parseAndValidatateMetadata(CustomMethod.class.getDeclaredMethod("patch")).template()) + @Test + public void customMethodWithoutPath() throws Exception { + assertThat(contract.parseAndValidatateMetadata(CustomMethod.class.getDeclaredMethod("patch")) + .template()) .hasMethod("PATCH") .hasUrl(""); } - interface WithQueryParamsInPath { - @RequestLine("GET /") Response none(); - - @RequestLine("GET /?Action=GetUser") Response one(); - - @RequestLine("GET /?Action=GetUser&Version=2010-05-08") Response two(); - - @RequestLine("GET /?Action=GetUser&Version=2010-05-08&limit=1") Response three(); - - @RequestLine("GET /?flag&Action=GetUser&Version=2010-05-08") Response empty(); - } - - @Test public void queryParamsInPathExtract() throws Exception { - assertThat(contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("none")).template()) + @Test + public void queryParamsInPathExtract() throws Exception { + assertThat( + contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("none")) + .template()) .hasUrl("/") .hasQueries(); - assertThat(contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("one")).template()) + assertThat( + contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("one")) + .template()) .hasUrl("/") .hasQueries( entry("Action", asList("GetUser")) ); - assertThat(contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("two")).template()) + assertThat( + contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("two")) + .template()) .hasUrl("/") .hasQueries( entry("Action", asList("GetUser")), entry("Version", asList("2010-05-08")) ); - assertThat(contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("three")).template()) + assertThat( + contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("three")) + .template()) .hasUrl("/") .hasQueries( entry("Action", asList("GetUser")), @@ -131,30 +123,32 @@ interface WithQueryParamsInPath { entry("limit", asList("1")) ); - assertThat(contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("empty")).template()) + assertThat( + contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("empty")) + .template()) .hasUrl("/") .hasQueries( - entry("flag", asList(new String[] { null })), + entry("flag", asList(new String[]{null})), entry("Action", asList("GetUser")), entry("Version", asList("2010-05-08")) ); } - interface BodyWithoutParameters { - @RequestLine("POST /") - @Headers("Content-Type: application/xml") - @Body("") Response post(); - } - - @Test public void bodyWithoutParameters() throws Exception { - MethodMetadata md = contract.parseAndValidatateMetadata(BodyWithoutParameters.class.getDeclaredMethod("post")); + @Test + public void bodyWithoutParameters() throws Exception { + MethodMetadata + md = + contract.parseAndValidatateMetadata(BodyWithoutParameters.class.getDeclaredMethod("post")); assertThat(md.template()) .hasBody(""); } - @Test public void producesAddsContentTypeHeader() throws Exception { - MethodMetadata md = contract.parseAndValidatateMetadata(BodyWithoutParameters.class.getDeclaredMethod("post")); + @Test + public void producesAddsContentTypeHeader() throws Exception { + MethodMetadata + md = + contract.parseAndValidatateMetadata(BodyWithoutParameters.class.getDeclaredMethod("post")); assertThat(md.template()) .hasHeaders( @@ -163,11 +157,8 @@ interface BodyWithoutParameters { ); } - interface WithURIParam { - @RequestLine("GET /{1}/{2}") Response uriParam(@Param("1") String one, URI endpoint, @Param("2") String two); - } - - @Test public void withPathAndURIParam() throws Exception { + @Test + public void withPathAndURIParam() throws Exception { MethodMetadata md = contract.parseAndValidatateMetadata( WithURIParam.class.getDeclaredMethod("uriParam", String.class, URI.class, String.class)); @@ -181,15 +172,12 @@ interface WithURIParam { assertThat(md.urlIndex()).isEqualTo(1); } - interface WithPathAndQueryParams { - @RequestLine("GET /domains/{domainId}/records?name={name}&type={type}") - Response recordsByNameAndType(@Param("domainId") int id, @Param("name") String nameFilter, - @Param("type") String typeFilter); - } - - @Test public void pathAndQueryParams() throws Exception { - MethodMetadata md = contract.parseAndValidatateMetadata(WithPathAndQueryParams.class.getDeclaredMethod - ("recordsByNameAndType", int.class, String.class, String.class)); + @Test + public void pathAndQueryParams() throws Exception { + MethodMetadata + md = + contract.parseAndValidatateMetadata(WithPathAndQueryParams.class.getDeclaredMethod + ("recordsByNameAndType", int.class, String.class, String.class)); assertThat(md.template()) .hasQueries(entry("name", asList("{name}")), entry("type", asList("{type}"))); @@ -201,25 +189,28 @@ Response recordsByNameAndType(@Param("domainId") int id, @Param("name") String n ); } - interface FormParams { - @RequestLine("POST /") - @Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D") - void login( - @Param("customer_name") String customer, - @Param("user_name") String user, @Param("password") String password); - } - - @Test public void bodyWithTemplate() throws Exception { - MethodMetadata md = contract.parseAndValidatateMetadata(FormParams.class.getDeclaredMethod("login", String.class, - String.class, String.class)); + @Test + public void bodyWithTemplate() throws Exception { + MethodMetadata + md = + contract + .parseAndValidatateMetadata(FormParams.class.getDeclaredMethod("login", String.class, + String.class, + String.class)); assertThat(md.template()) - .hasBodyTemplate("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D"); + .hasBodyTemplate( + "%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D"); } - @Test public void formParamsParseIntoIndexToName() throws Exception { - MethodMetadata md = contract.parseAndValidatateMetadata(FormParams.class.getDeclaredMethod("login", String.class, - String.class, String.class)); + @Test + public void formParamsParseIntoIndexToName() throws Exception { + MethodMetadata + md = + contract + .parseAndValidatateMetadata(FormParams.class.getDeclaredMethod("login", String.class, + String.class, + String.class)); assertThat(md.formParams()) .containsExactly("customer_name", "user_name", "password"); @@ -231,22 +222,27 @@ void login( ); } - /** Body type is only for the body param. */ - @Test public void formParamsDoesNotSetBodyType() throws Exception { - MethodMetadata md = contract.parseAndValidatateMetadata(FormParams.class.getDeclaredMethod("login", String.class, - String.class, String.class)); + /** + * Body type is only for the body param. + */ + @Test + public void formParamsDoesNotSetBodyType() throws Exception { + MethodMetadata + md = + contract + .parseAndValidatateMetadata(FormParams.class.getDeclaredMethod("login", String.class, + String.class, + String.class)); assertThat(md.bodyType()).isNull(); } - interface HeaderParams { - @RequestLine("POST /") - @Headers({"Auth-Token: {Auth-Token}", "Auth-Token: Foo"}) - void logout(@Param("Auth-Token") String token); - } - - @Test public void headerParamsParseIntoIndexToName() throws Exception { - MethodMetadata md = contract.parseAndValidatateMetadata(HeaderParams.class.getDeclaredMethod("logout", String.class)); + @Test + public void headerParamsParseIntoIndexToName() throws Exception { + MethodMetadata + md = + contract.parseAndValidatateMetadata( + HeaderParams.class.getDeclaredMethod("logout", String.class)); assertThat(md.template()) .hasHeaders(entry("Auth-Token", asList("{Auth-Token}", "Foo"))); @@ -255,20 +251,113 @@ interface HeaderParams { .containsExactly(entry(0, asList("Auth-Token"))); } + @Test + public void customExpander() throws Exception { + MethodMetadata + md = + contract + .parseAndValidatateMetadata(CustomExpander.class.getDeclaredMethod("date", Date.class)); + + assertThat(md.indexToExpanderClass()) + .containsExactly(entry(0, DateToMillis.class)); + } + + interface Methods { + + @RequestLine("POST /") + void post(); + + @RequestLine("PUT /") + void put(); + + @RequestLine("GET /") + void get(); + + @RequestLine("DELETE /") + void delete(); + } + + interface BodyParams { + + @RequestLine("POST") + Response post(List body); + + @RequestLine("POST") + Response tooMany(List body, List body2); + } + + interface CustomMethod { + + @RequestLine("PATCH") + Response patch(); + } + + interface WithQueryParamsInPath { + + @RequestLine("GET /") + Response none(); + + @RequestLine("GET /?Action=GetUser") + Response one(); + + @RequestLine("GET /?Action=GetUser&Version=2010-05-08") + Response two(); + + @RequestLine("GET /?Action=GetUser&Version=2010-05-08&limit=1") + Response three(); + + @RequestLine("GET /?flag&Action=GetUser&Version=2010-05-08") + Response empty(); + } + + interface BodyWithoutParameters { + + @RequestLine("POST /") + @Headers("Content-Type: application/xml") + @Body("") + Response post(); + } + + interface WithURIParam { + + @RequestLine("GET /{1}/{2}") + Response uriParam(@Param("1") String one, URI endpoint, @Param("2") String two); + } + + interface WithPathAndQueryParams { + + @RequestLine("GET /domains/{domainId}/records?name={name}&type={type}") + Response recordsByNameAndType(@Param("domainId") int id, @Param("name") String nameFilter, + @Param("type") String typeFilter); + } + + interface FormParams { + + @RequestLine("POST /") + @Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D") + void login( + @Param("customer_name") String customer, + @Param("user_name") String user, @Param("password") String password); + } + + interface HeaderParams { + + @RequestLine("POST /") + @Headers({"Auth-Token: {Auth-Token}", "Auth-Token: Foo"}) + void logout(@Param("Auth-Token") String token); + } + interface CustomExpander { - @RequestLine("POST /?date={date}") void date(@Param(value = "date", expander = DateToMillis.class) Date date); + + @RequestLine("POST /?date={date}") + void date(@Param(value = "date", expander = DateToMillis.class) Date date); } class DateToMillis implements Param.Expander { - @Override public String expand(Object value) { + + @Override + public String expand(Object value) { return String.valueOf(((Date) value).getTime()); } } - - @Test public void customExpander() throws Exception { - MethodMetadata md = contract.parseAndValidatateMetadata(CustomExpander.class.getDeclaredMethod("date", Date.class)); - - assertThat(md.indexToExpanderClass()) - .containsExactly(entry(0, DateToMillis.class)); - } } diff --git a/core/src/test/java/feign/DefaultRetryerTest.java b/core/src/test/java/feign/DefaultRetryerTest.java index a73cdbed4f..0d5702a10a 100644 --- a/core/src/test/java/feign/DefaultRetryerTest.java +++ b/core/src/test/java/feign/DefaultRetryerTest.java @@ -18,15 +18,20 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; + import java.util.Date; + import feign.Retryer.Default; import static org.junit.Assert.assertEquals; public class DefaultRetryerTest { - @Rule public final ExpectedException thrown = ExpectedException.none(); - @Test public void only5TriesAllowedAndExponentialBackoff() throws Exception { + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Test + public void only5TriesAllowedAndExponentialBackoff() throws Exception { RetryableException e = new RetryableException(null, null, null); Default retryer = new Retryer.Default(); assertEquals(1, retryer.attempt); @@ -52,7 +57,8 @@ public class DefaultRetryerTest { retryer.continueOrPropagate(e); } - @Test public void considersRetryAfterButNotMoreThanMaxPeriod() throws Exception { + @Test + public void considersRetryAfterButNotMoreThanMaxPeriod() throws Exception { Default retryer = new Retryer.Default() { protected long currentTimeMillis() { return 0; diff --git a/core/src/test/java/feign/EmptyTargetTest.java b/core/src/test/java/feign/EmptyTargetTest.java index b90a71ba7a..a36968ed59 100644 --- a/core/src/test/java/feign/EmptyTargetTest.java +++ b/core/src/test/java/feign/EmptyTargetTest.java @@ -15,40 +15,51 @@ */ package feign; -import feign.Target.EmptyTarget; -import java.net.URI; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import java.net.URI; + +import feign.Target.EmptyTarget; + import static feign.assertj.FeignAssertions.assertThat; public class EmptyTargetTest { - @Rule public final ExpectedException thrown = ExpectedException.none(); - interface UriInterface { - @RequestLine("GET /") Response get(URI endpoint); - } + @Rule + public final ExpectedException thrown = ExpectedException.none(); - @Test public void whenNameNotSupplied() { + @Test + public void whenNameNotSupplied() { assertThat(EmptyTarget.create(UriInterface.class)) .isEqualTo(EmptyTarget.create(UriInterface.class, "empty:UriInterface")); } - @Test public void toString_withoutName() { + @Test + public void toString_withoutName() { assertThat(EmptyTarget.create(UriInterface.class).toString()) .isEqualTo("EmptyTarget(type=UriInterface)"); } - @Test public void toString_withName() { + @Test + public void toString_withName() { assertThat(EmptyTarget.create(UriInterface.class, "manager-access").toString()) .isEqualTo("EmptyTarget(type=UriInterface, name=manager-access)"); } - @Test public void mustApplyToAbsoluteUrl() { + @Test + public void mustApplyToAbsoluteUrl() { thrown.expect(UnsupportedOperationException.class); thrown.expectMessage("Request with non-absolute URL not supported with empty target"); - EmptyTarget.create(UriInterface.class).apply(new RequestTemplate().method("GET").append("/relative")); + EmptyTarget.create(UriInterface.class) + .apply(new RequestTemplate().method("GET").append("/relative")); + } + + interface UriInterface { + + @RequestLine("GET /") + Response get(URI endpoint); } } diff --git a/core/src/test/java/feign/FeignBuilderTest.java b/core/src/test/java/feign/FeignBuilderTest.java index 63d452ea03..d834231b2e 100644 --- a/core/src/test/java/feign/FeignBuilderTest.java +++ b/core/src/test/java/feign/FeignBuilderTest.java @@ -17,8 +17,10 @@ import com.squareup.okhttp.mockwebserver.MockResponse; import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule; -import feign.codec.Decoder; -import feign.codec.Encoder; + +import org.junit.Rule; +import org.junit.Test; + import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Type; @@ -26,24 +28,20 @@ import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; -import org.junit.Rule; -import org.junit.Test; + +import feign.codec.Decoder; +import feign.codec.Encoder; import static feign.assertj.MockWebServerAssertions.assertThat; import static org.junit.Assert.assertEquals; public class FeignBuilderTest { - @Rule public final MockWebServerRule server = new MockWebServerRule(); - - interface TestInterface { - @RequestLine("POST /") Response codecPost(String data); - - @RequestLine("POST /") void encodedPost(List data); - @RequestLine("POST /") String decodedPost(); - } + @Rule + public final MockWebServerRule server = new MockWebServerRule(); - @Test public void testDefaults() throws Exception { + @Test + public void testDefaults() throws Exception { server.enqueue(new MockResponse().setBody("response data")); String url = "http://localhost:" + server.getPort(); @@ -56,12 +54,14 @@ interface TestInterface { .hasBody("request data"); } - @Test public void testOverrideEncoder() throws Exception { + @Test + public void testOverrideEncoder() throws Exception { server.enqueue(new MockResponse().setBody("response data")); String url = "http://localhost:" + server.getPort(); Encoder encoder = new Encoder() { - @Override public void encode(Object object, Type bodyType, RequestTemplate template) { + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) { template.body(object.toString()); } }; @@ -73,7 +73,8 @@ interface TestInterface { .hasBody("[This, is, my, request]"); } - @Test public void testOverrideDecoder() throws Exception { + @Test + public void testOverrideDecoder() throws Exception { server.enqueue(new MockResponse().setBody("success!")); String url = "http://localhost:" + server.getPort(); @@ -90,7 +91,8 @@ public Object decode(Response response, Type type) { assertEquals(1, server.getRequestCount()); } - @Test public void testProvideRequestInterceptors() throws Exception { + @Test + public void testProvideRequestInterceptors() throws Exception { server.enqueue(new MockResponse().setBody("response data")); String url = "http://localhost:" + server.getPort(); @@ -101,7 +103,9 @@ public void apply(RequestTemplate template) { } }; - TestInterface api = Feign.builder().requestInterceptor(requestInterceptor).target(TestInterface.class, url); + TestInterface + api = + Feign.builder().requestInterceptor(requestInterceptor).target(TestInterface.class, url); Response response = api.codecPost("request data"); assertEquals(Util.toString(response.body().asReader()), "response data"); @@ -110,7 +114,8 @@ public void apply(RequestTemplate template) { .hasBody("request data"); } - @Test public void testProvideInvocationHandlerFactory() throws Exception { + @Test + public void testProvideInvocationHandlerFactory() throws Exception { server.enqueue(new MockResponse().setBody("response data")); String url = "http://localhost:" + server.getPort(); @@ -118,13 +123,17 @@ public void apply(RequestTemplate template) { final AtomicInteger callCount = new AtomicInteger(); InvocationHandlerFactory factory = new InvocationHandlerFactory() { private final InvocationHandlerFactory delegate = new Default(); - @Override public InvocationHandler create(Target target, Map dispatch) { + + @Override + public InvocationHandler create(Target target, Map dispatch) { callCount.incrementAndGet(); return delegate.create(target, dispatch); } }; - TestInterface api = Feign.builder().invocationHandlerFactory(factory).target(TestInterface.class, url); + TestInterface + api = + Feign.builder().invocationHandlerFactory(factory).target(TestInterface.class, url); Response response = api.codecPost("request data"); assertEquals("response data", Util.toString(response.body().asReader())); assertEquals(1, callCount.get()); @@ -132,4 +141,16 @@ public void apply(RequestTemplate template) { assertThat(server.takeRequest()) .hasBody("request data"); } + + interface TestInterface { + + @RequestLine("POST /") + Response codecPost(String data); + + @RequestLine("POST /") + void encodedPost(List data); + + @RequestLine("POST /") + String decodedPost(); + } } diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index 47e5e0914e..5fea589de1 100644 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -17,15 +17,15 @@ import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; + import com.squareup.okhttp.mockwebserver.MockResponse; import com.squareup.okhttp.mockwebserver.SocketPolicy; import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule; -import feign.Target.HardCodedTarget; -import feign.codec.Decoder; -import feign.codec.EncodeException; -import feign.codec.Encoder; -import feign.codec.ErrorDecoder; -import feign.codec.StringDecoder; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + import java.io.IOException; import java.lang.reflect.Type; import java.net.URI; @@ -34,9 +34,13 @@ import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; + +import feign.Target.HardCodedTarget; +import feign.codec.Decoder; +import feign.codec.EncodeException; +import feign.codec.Encoder; +import feign.codec.ErrorDecoder; +import feign.codec.StringDecoder; import static feign.Util.UTF_8; import static feign.assertj.MockWebServerAssertions.assertThat; @@ -44,40 +48,14 @@ import static org.junit.Assert.assertTrue; public class FeignTest { - @Rule public final ExpectedException thrown = ExpectedException.none(); - @Rule public final MockWebServerRule server = new MockWebServerRule(); - - interface TestInterface { - @RequestLine("POST /") Response response(); - - @RequestLine("POST /") String post(); - - @RequestLine("POST /") - @Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D") - void login( - @Param("customer_name") String customer, @Param("user_name") String user, @Param("password") String password); - - @RequestLine("POST /") void body(List contents); - - @RequestLine("POST /") @Headers("Content-Encoding: gzip") void gzipBody(List contents); - @RequestLine("POST /") void form( - @Param("customer_name") String customer, @Param("user_name") String user, @Param("password") String password); + @Rule + public final ExpectedException thrown = ExpectedException.none(); + @Rule + public final MockWebServerRule server = new MockWebServerRule(); - @RequestLine("GET /{1}/{2}") Response uriParam(@Param("1") String one, URI endpoint, @Param("2") String two); - - @RequestLine("GET /?1={1}&2={2}") Response queryParams(@Param("1") String one, @Param("2") Iterable twos); - - @RequestLine("POST /?date={date}") void expand(@Param(value = "date", expander = DateToMillis.class) Date date); - - class DateToMillis implements Param.Expander { - @Override public String expand(Object value) { - return String.valueOf(((Date) value).getTime()); - } - } - } - - @Test public void iterableQueryParams() throws IOException, InterruptedException { + @Test + public void iterableQueryParams() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("foo")); TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); @@ -88,26 +66,21 @@ class DateToMillis implements Param.Expander { .hasPath("/?1=user&2=apple&2=pear"); } - interface OtherTestInterface { - @RequestLine("POST /") String post(); - - @RequestLine("POST /") byte[] binaryResponseBody(); - - @RequestLine("POST /") void binaryRequestBody(byte[] contents); - } - - @Test public void postTemplateParamsResolve() throws IOException, InterruptedException { + @Test + public void postTemplateParamsResolve() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("foo")); - + TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); api.login("netflix", "denominator", "password"); assertThat(server.takeRequest()) - .hasBody("{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}"); + .hasBody( + "{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}"); } - @Test public void responseCoercesToStringBody() throws IOException, InterruptedException { + @Test + public void responseCoercesToStringBody() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("foo")); TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); @@ -117,7 +90,8 @@ interface OtherTestInterface { assertEquals("foo", response.body().toString()); } - @Test public void postFormParams() throws IOException, InterruptedException { + @Test + public void postFormParams() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("foo")); TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); @@ -125,10 +99,12 @@ interface OtherTestInterface { api.form("netflix", "denominator", "password"); assertThat(server.takeRequest()) - .hasBody("{\"customer_name\":\"netflix\",\"user_name\":\"denominator\",\"password\":\"password\"}"); + .hasBody( + "{\"customer_name\":\"netflix\",\"user_name\":\"denominator\",\"password\":\"password\"}"); } - @Test public void postBodyParam() throws IOException, InterruptedException { + @Test + public void postBodyParam() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("foo")); TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); @@ -140,14 +116,20 @@ interface OtherTestInterface { .hasBody("[netflix, denominator, password]"); } - /** The type of a parameter value may not be the desired type to encode as. Prefer the interface type. */ - @Test public void bodyTypeCorrespondsWithParameterType() throws IOException, InterruptedException { + /** + * The type of a parameter value may not be the desired type to encode as. Prefer the interface + * type. + */ + @Test + public void bodyTypeCorrespondsWithParameterType() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("foo")); final AtomicReference encodedType = new AtomicReference(); TestInterface api = new TestInterfaceBuilder() .encoder(new Encoder.Default() { - @Override public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException { + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) + throws EncodeException { encodedType.set(bodyType); } }) @@ -157,10 +139,12 @@ interface OtherTestInterface { server.takeRequest(); - assertThat(encodedType.get()).isEqualTo(new TypeToken>(){}.getType()); + assertThat(encodedType.get()).isEqualTo(new TypeToken>() { + }.getType()); } - @Test public void postGZIPEncodedBodyParam() throws IOException, InterruptedException { + @Test + public void postGZIPEncodedBodyParam() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("foo")); TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); @@ -172,15 +156,10 @@ interface OtherTestInterface { .hasGzippedBody("[netflix, denominator, password]".getBytes(UTF_8)); } - static class ForwardedForInterceptor implements RequestInterceptor { - @Override public void apply(RequestTemplate template) { - template.header("X-Forwarded-For", "origin.host.com"); - } - } - - @Test public void singleInterceptor() throws IOException, InterruptedException { + @Test + public void singleInterceptor() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("foo")); - + TestInterface api = new TestInterfaceBuilder() .requestInterceptor(new ForwardedForInterceptor()) .target("http://localhost:" + server.getPort()); @@ -191,13 +170,8 @@ static class ForwardedForInterceptor implements RequestInterceptor { .hasHeaders("X-Forwarded-For: origin.host.com"); } - static class UserAgentInterceptor implements RequestInterceptor { - @Override public void apply(RequestTemplate template) { - template.header("User-Agent", "Feign"); - } - } - - @Test public void multipleInterceptor() throws IOException, InterruptedException { + @Test + public void multipleInterceptor() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("foo")); TestInterface api = new TestInterfaceBuilder() @@ -207,10 +181,12 @@ static class UserAgentInterceptor implements RequestInterceptor { api.post(); - assertThat(server.takeRequest()).hasHeaders("X-Forwarded-For: origin.host.com", "User-Agent: Feign"); + assertThat(server.takeRequest()).hasHeaders("X-Forwarded-For: origin.host.com", + "User-Agent: Feign"); } - @Test public void customExpander() throws Exception { + @Test + public void customExpander() throws Exception { server.enqueue(new MockResponse()); TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); @@ -221,20 +197,18 @@ static class UserAgentInterceptor implements RequestInterceptor { .hasPath("/?date=1234"); } - @Test public void toKeyMethodFormatsAsExpected() throws Exception { - assertEquals("TestInterface#post()", Feign.configKey(TestInterface.class.getDeclaredMethod("post"))); + @Test + public void toKeyMethodFormatsAsExpected() throws Exception { + assertEquals("TestInterface#post()", + Feign.configKey(TestInterface.class.getDeclaredMethod("post"))); assertEquals("TestInterface#uriParam(String,URI,String)", - Feign.configKey(TestInterface.class.getDeclaredMethod("uriParam", String.class, URI.class, String.class))); - } - - static class IllegalArgumentExceptionOn404 extends 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); - } + Feign.configKey(TestInterface.class + .getDeclaredMethod("uriParam", String.class, URI.class, + String.class))); } - @Test public void canOverrideErrorDecoder() throws IOException, InterruptedException { + @Test + public void canOverrideErrorDecoder() throws IOException, InterruptedException { server.enqueue(new MockResponse().setResponseCode(404).setBody("foo")); thrown.expect(IllegalArgumentException.class); thrown.expectMessage("zone not found"); @@ -246,7 +220,8 @@ static class IllegalArgumentExceptionOn404 extends ErrorDecoder.Default { api.post(); } - @Test public void retriesLostConnectionBeforeRead() throws IOException, InterruptedException { + @Test + public void retriesLostConnectionBeforeRead() throws IOException, InterruptedException { server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); server.enqueue(new MockResponse().setBody("success!")); @@ -257,12 +232,14 @@ static class IllegalArgumentExceptionOn404 extends ErrorDecoder.Default { assertEquals(2, server.getRequestCount()); } - @Test public void overrideTypeSpecificDecoder() throws IOException, InterruptedException { + @Test + public void overrideTypeSpecificDecoder() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("success!")); - + TestInterface api = new TestInterfaceBuilder() .decoder(new Decoder() { - @Override public Object decode(Response response, Type type) { + @Override + public Object decode(Response response, Type type) { return "fail"; } }).target("http://localhost:" + server.getPort()); @@ -273,15 +250,19 @@ static class IllegalArgumentExceptionOn404 extends ErrorDecoder.Default { /** * when you must parse a 2xx status to determine if the operation succeeded or not. */ - @Test public void retryableExceptionInDecoder() throws IOException, InterruptedException { + @Test + public void retryableExceptionInDecoder() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("retry!")); server.enqueue(new MockResponse().setBody("success!")); - + TestInterface api = new TestInterfaceBuilder() .decoder(new StringDecoder() { - @Override public Object decode(Response response, Type type) throws IOException { + @Override + public Object decode(Response response, Type type) throws IOException { String string = super.decode(response, type).toString(); - if ("retry!".equals(string)) throw new RetryableException(string, null); + if ("retry!".equals(string)) { + throw new RetryableException(string, null); + } return string; } }).target("http://localhost:" + server.getPort()); @@ -290,15 +271,16 @@ static class IllegalArgumentExceptionOn404 extends ErrorDecoder.Default { assertEquals(2, server.getRequestCount()); } - - @Test public void doesntRetryAfterResponseIsSent() throws IOException, InterruptedException { + @Test + public void doesntRetryAfterResponseIsSent() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("success!")); thrown.expect(FeignException.class); thrown.expectMessage("error reading response POST http://"); TestInterface api = new TestInterfaceBuilder() .decoder(new Decoder() { - @Override public Object decode(Response response, Type type) throws IOException { + @Override + public Object decode(Response response, Type type) throws IOException { throw new IOException("error reading response"); } }).target("http://localhost:" + server.getPort()); @@ -310,9 +292,14 @@ static class IllegalArgumentExceptionOn404 extends ErrorDecoder.Default { } } - @Test public void equalsHashCodeAndToStringWork() { - Target t1 = new HardCodedTarget(TestInterface.class, "http://localhost:8080"); - Target t2 = new HardCodedTarget(TestInterface.class, "http://localhost:8888"); + @Test + public void equalsHashCodeAndToStringWork() { + Target + t1 = + new HardCodedTarget(TestInterface.class, "http://localhost:8080"); + Target + t2 = + new HardCodedTarget(TestInterface.class, "http://localhost:8888"); Target t3 = new HardCodedTarget(OtherTestInterface.class, "http://localhost:8080"); TestInterface i1 = Feign.builder().target(t1); @@ -345,21 +332,27 @@ static class IllegalArgumentExceptionOn404 extends ErrorDecoder.Default { .isEqualTo(i1.toString()); } - @Test public void decodeLogicSupportsByteArray() throws Exception { + @Test + public void decodeLogicSupportsByteArray() throws Exception { byte[] expectedResponse = {12, 34, 56}; server.enqueue(new MockResponse().setBody(expectedResponse)); - OtherTestInterface api = Feign.builder().target(OtherTestInterface.class, "http://localhost:" + server.getPort()); + OtherTestInterface + api = + Feign.builder().target(OtherTestInterface.class, "http://localhost:" + server.getPort()); assertThat(api.binaryResponseBody()) .containsExactly(expectedResponse); } - @Test public void encodeLogicSupportsByteArray() throws Exception { + @Test + public void encodeLogicSupportsByteArray() throws Exception { byte[] expectedRequest = {12, 34, 56}; server.enqueue(new MockResponse()); - OtherTestInterface api = Feign.builder().target(OtherTestInterface.class, "http://localhost:" + server.getPort()); + OtherTestInterface + api = + Feign.builder().target(OtherTestInterface.class, "http://localhost:" + server.getPort()); api.binaryRequestBody(expectedRequest); @@ -367,11 +360,97 @@ static class IllegalArgumentExceptionOn404 extends ErrorDecoder.Default { .hasBody(expectedRequest); } + interface TestInterface { + + @RequestLine("POST /") + Response response(); + + @RequestLine("POST /") + String post(); + + @RequestLine("POST /") + @Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D") + void login( + @Param("customer_name") String customer, @Param("user_name") String user, + @Param("password") String password); + + @RequestLine("POST /") + void body(List contents); + + @RequestLine("POST /") + @Headers("Content-Encoding: gzip") + void gzipBody(List contents); + + @RequestLine("POST /") + void form( + @Param("customer_name") String customer, @Param("user_name") String user, + @Param("password") String password); + + @RequestLine("GET /{1}/{2}") + Response uriParam(@Param("1") String one, URI endpoint, @Param("2") String two); + + @RequestLine("GET /?1={1}&2={2}") + Response queryParams(@Param("1") String one, @Param("2") Iterable twos); + + @RequestLine("POST /?date={date}") + void expand(@Param(value = "date", expander = DateToMillis.class) Date date); + + class DateToMillis implements Param.Expander { + + @Override + public String expand(Object value) { + return String.valueOf(((Date) value).getTime()); + } + } + } + + + interface OtherTestInterface { + + @RequestLine("POST /") + String post(); + + @RequestLine("POST /") + byte[] binaryResponseBody(); + + @RequestLine("POST /") + void binaryRequestBody(byte[] contents); + } + + static class ForwardedForInterceptor implements RequestInterceptor { + + @Override + public void apply(RequestTemplate template) { + template.header("X-Forwarded-For", "origin.host.com"); + } + } + + static class UserAgentInterceptor implements RequestInterceptor { + + @Override + public void apply(RequestTemplate template) { + template.header("User-Agent", "Feign"); + } + } + + static class IllegalArgumentExceptionOn404 extends 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); + } + } + static final class TestInterfaceBuilder { + private final Feign.Builder delegate = new Feign.Builder() .decoder(new Decoder.Default()) .encoder(new Encoder() { - @Override public void encode(Object object, Type bodyType, RequestTemplate template) { + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) { if (object instanceof Map) { template.body(new Gson().toJson(object)); } else { diff --git a/core/src/test/java/feign/LoggerTest.java b/core/src/test/java/feign/LoggerTest.java index 69aff9aabc..ea9826ed4f 100644 --- a/core/src/test/java/feign/LoggerTest.java +++ b/core/src/test/java/feign/LoggerTest.java @@ -17,12 +17,7 @@ import com.squareup.okhttp.mockwebserver.MockResponse; import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule; -import feign.Logger.Level; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.TimeUnit; + import org.assertj.core.api.SoftAssertions; import org.junit.Rule; import org.junit.Test; @@ -35,13 +30,26 @@ import org.junit.runners.Parameterized.Parameters; import org.junit.runners.model.Statement; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import feign.Logger.Level; + @RunWith(Enclosed.class) public class LoggerTest { - @Rule public final MockWebServerRule server = new MockWebServerRule(); - @Rule public final RecordingLogger logger = new RecordingLogger(); - @Rule public final ExpectedException thrown = ExpectedException.none(); + + @Rule + public final MockWebServerRule server = new MockWebServerRule(); + @Rule + public final RecordingLogger logger = new RecordingLogger(); + @Rule + public final ExpectedException thrown = ExpectedException.none(); interface SendsStuff { + @RequestLine("POST /") @Headers("Content-Type: application/json") @Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D") @@ -52,24 +60,30 @@ String login( @RunWith(Parameterized.class) public static class LogLevelEmitsTest extends LoggerTest { + private final Level logLevel; + public LogLevelEmitsTest(Level logLevel, List expectedMessages) { + this.logLevel = logLevel; + logger.expectMessages(expectedMessages); + } + @Parameters public static Iterable data() { - return Arrays.asList(new Object[][] { - { Level.NONE, Arrays.asList() }, - { Level.BASIC, Arrays.asList( + return Arrays.asList(new Object[][]{ + {Level.NONE, Arrays.asList()}, + {Level.BASIC, Arrays.asList( "\\[SendsStuff#login\\] ---> POST http://localhost:[0-9]+/ HTTP/1.1", - "\\[SendsStuff#login\\] <--- HTTP/1.1 200 OK \\([0-9]+ms\\)") }, - { Level.HEADERS, Arrays.asList( + "\\[SendsStuff#login\\] <--- HTTP/1.1 200 OK \\([0-9]+ms\\)")}, + {Level.HEADERS, 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\\] <--- END HTTP \\(3-byte body\\)") }, - { Level.FULL, Arrays.asList( + "\\[SendsStuff#login\\] <--- END HTTP \\(3-byte body\\)")}, + {Level.FULL, Arrays.asList( "\\[SendsStuff#login\\] ---> POST http://localhost:[0-9]+/ HTTP/1.1", "\\[SendsStuff#login\\] Content-Type: application/json", "\\[SendsStuff#login\\] Content-Length: 80", @@ -80,16 +94,12 @@ public static Iterable data() { "\\[SendsStuff#login\\] Content-Length: 3", "\\[SendsStuff#login\\] ", "\\[SendsStuff#login\\] foo", - "\\[SendsStuff#login\\] <--- END HTTP \\(3-byte body\\)") } + "\\[SendsStuff#login\\] <--- END HTTP \\(3-byte body\\)")} }); } - public LogLevelEmitsTest(Level logLevel, List expectedMessages) { - this.logLevel = logLevel; - logger.expectMessages(expectedMessages); - } - - @Test public void levelEmits() throws IOException, InterruptedException { + @Test + public void levelEmits() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("foo")); SendsStuff api = Feign.builder() @@ -103,25 +113,31 @@ public LogLevelEmitsTest(Level logLevel, List expectedMessages) { @RunWith(Parameterized.class) public static class ReadTimeoutEmitsTest extends LoggerTest { + private final Level logLevel; + public ReadTimeoutEmitsTest(Level logLevel, List expectedMessages) { + this.logLevel = logLevel; + logger.expectMessages(expectedMessages); + } + @Parameters public static Iterable data() { - return Arrays.asList(new Object[][] { - { Level.NONE, Arrays.asList() }, - { Level.BASIC, Arrays.asList( + return Arrays.asList(new Object[][]{ + {Level.NONE, Arrays.asList()}, + {Level.BASIC, 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\\)") }, - { Level.HEADERS, Arrays.asList( + "\\[SendsStuff#login\\] <--- ERROR SocketTimeoutException: Read timed out \\([0-9]+ms\\)")}, + {Level.HEADERS, 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\\)") }, - { Level.FULL, Arrays.asList( + "\\[SendsStuff#login\\] <--- ERROR SocketTimeoutException: Read timed out \\([0-9]+ms\\)")}, + {Level.FULL, Arrays.asList( "\\[SendsStuff#login\\] ---> POST http://localhost:[0-9]+/ HTTP/1.1", "\\[SendsStuff#login\\] Content-Type: application/json", "\\[SendsStuff#login\\] Content-Length: 80", @@ -133,16 +149,12 @@ public static Iterable data() { "\\[SendsStuff#login\\] ", "\\[SendsStuff#login\\] <--- ERROR SocketTimeoutException: Read timed out \\([0-9]+ms\\)", "\\[SendsStuff#login\\] java.net.SocketTimeoutException: Read timed out.*", - "\\[SendsStuff#login\\] <--- END ERROR") } + "\\[SendsStuff#login\\] <--- END ERROR")} }); } - public ReadTimeoutEmitsTest(Level logLevel, List expectedMessages) { - this.logLevel = logLevel; - logger.expectMessages(expectedMessages); - } - - @Test public void levelEmitsOnReadTimeout() throws IOException, InterruptedException { + @Test + public void levelEmitsOnReadTimeout() throws IOException, InterruptedException { server.enqueue(new MockResponse().throttleBody(1, 1, TimeUnit.SECONDS).setBody("foo")); thrown.expect(FeignException.class); @@ -158,22 +170,28 @@ public ReadTimeoutEmitsTest(Level logLevel, List expectedMessages) { @RunWith(Parameterized.class) public static class UnknownHostEmitsTest extends LoggerTest { + private final Level logLevel; + public UnknownHostEmitsTest(Level logLevel, List expectedMessages) { + this.logLevel = logLevel; + logger.expectMessages(expectedMessages); + } + @Parameters public static Iterable data() { - return Arrays.asList(new Object[][] { - { Level.NONE, Arrays.asList() }, - { Level.BASIC, Arrays.asList( + return Arrays.asList(new Object[][]{ + {Level.NONE, Arrays.asList()}, + {Level.BASIC, Arrays.asList( "\\[SendsStuff#login\\] ---> POST http://robofu.abc/ HTTP/1.1", - "\\[SendsStuff#login\\] <--- ERROR UnknownHostException: robofu.abc \\([0-9]+ms\\)") }, - { Level.HEADERS, Arrays.asList( + "\\[SendsStuff#login\\] <--- ERROR UnknownHostException: robofu.abc \\([0-9]+ms\\)")}, + {Level.HEADERS, 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\\)") }, - { Level.FULL, Arrays.asList( + "\\[SendsStuff#login\\] <--- ERROR UnknownHostException: robofu.abc \\([0-9]+ms\\)")}, + {Level.FULL, Arrays.asList( "\\[SendsStuff#login\\] ---> POST http://robofu.abc/ HTTP/1.1", "\\[SendsStuff#login\\] Content-Type: application/json", "\\[SendsStuff#login\\] Content-Length: 80", @@ -182,21 +200,18 @@ public static Iterable data() { "\\[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") } + "\\[SendsStuff#login\\] <--- END ERROR")} }); } - public UnknownHostEmitsTest(Level logLevel, List expectedMessages) { - this.logLevel = logLevel; - logger.expectMessages(expectedMessages); - } - - @Test public void unknownHostEmits() throws IOException, InterruptedException { + @Test + public void unknownHostEmits() throws IOException, InterruptedException { SendsStuff api = Feign.builder() .logger(logger) .logLevel(logLevel) .retryer(new Retryer() { - @Override public void continueOrPropagate(RetryableException e) { + @Override + public void continueOrPropagate(RetryableException e) { throw e; } }) @@ -210,27 +225,29 @@ public UnknownHostEmitsTest(Level logLevel, List expectedMessages) { @RunWith(Parameterized.class) public static class RetryEmitsTest extends LoggerTest { + private final Level logLevel; + public RetryEmitsTest(Level logLevel, List expectedMessages) { + this.logLevel = logLevel; + logger.expectMessages(expectedMessages); + } + @Parameters public static Iterable data() { - return Arrays.asList(new Object[][] { - { Level.NONE, Arrays.asList() }, - { Level.BASIC, Arrays.asList( + return Arrays.asList(new Object[][]{ + {Level.NONE, Arrays.asList()}, + {Level.BASIC, 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\\)") } + "\\[SendsStuff#login\\] <--- ERROR UnknownHostException: robofu.abc \\([0-9]+ms\\)")} }); } - public RetryEmitsTest(Level logLevel, List expectedMessages) { - this.logLevel = logLevel; - logger.expectMessages(expectedMessages); - } - - @Test public void retryEmits() throws IOException, InterruptedException { + @Test + public void retryEmits() throws IOException, InterruptedException { thrown.expect(FeignException.class); SendsStuff api = Feign.builder() @@ -239,7 +256,8 @@ public RetryEmitsTest(Level logLevel, List expectedMessages) { .retryer(new Retryer() { boolean retried; - @Override public void continueOrPropagate(RetryableException e) { + @Override + public void continueOrPropagate(RetryableException e) { if (!retried) { retried = true; return; @@ -254,21 +272,25 @@ public RetryEmitsTest(Level logLevel, List expectedMessages) { } private static final class RecordingLogger extends Logger implements TestRule { + private final List messages = new ArrayList(); private final List expectedMessages = new ArrayList(); - RecordingLogger expectMessages(List expectedMessages){ + RecordingLogger expectMessages(List expectedMessages) { this.expectedMessages.addAll(expectedMessages); return this; } - - @Override protected void log(String configKey, String format, Object... args) { + + @Override + protected void log(String configKey, String format, Object... args) { messages.add(methodTag(configKey) + String.format(format, args)); } - @Override public Statement apply(final Statement base, Description description) { + @Override + public Statement apply(final Statement base, Description description) { return new Statement() { - @Override public void evaluate() throws Throwable { + @Override + public void evaluate() throws Throwable { base.evaluate(); SoftAssertions softly = new SoftAssertions(); for (int i = 0; i < messages.size(); i++) { diff --git a/core/src/test/java/feign/RequestTemplateTest.java b/core/src/test/java/feign/RequestTemplateTest.java index 032c82c6eb..a4893f7ea2 100644 --- a/core/src/test/java/feign/RequestTemplateTest.java +++ b/core/src/test/java/feign/RequestTemplateTest.java @@ -15,41 +15,69 @@ */ package feign; +import org.junit.Test; + import java.util.Arrays; import java.util.LinkedHashMap; import java.util.Map; -import org.junit.Test; -import static feign.assertj.FeignAssertions.assertThat; import static feign.RequestTemplate.expand; +import static feign.assertj.FeignAssertions.assertThat; import static java.util.Arrays.asList; import static org.assertj.core.data.MapEntry.entry; public class RequestTemplateTest { - - @Test public void expandNotUrlEncoded() { + + /** + * Avoid depending on guava solely for map literals. + */ + private static Map mapOf(String key, Object val) { + Map result = new LinkedHashMap(); + result.put(key, val); + return result; + } + + private static Map mapOf(String k1, Object v1, String k2, Object v2) { + Map result = mapOf(k1, v1); + result.put(k2, v2); + return result; + } + + private static Map mapOf(String k1, Object v1, String k2, Object v2, String k3, + Object v3) { + Map result = mapOf(k1, v1, k2, v2); + result.put(k3, v3); + return result; + } + + @Test + public void expandNotUrlEncoded() { for (String val : Arrays.asList("apples", "sp ace", "unic???de", "qu?stion")) { assertThat(expand("/users/{user}", mapOf("user", val))) .isEqualTo("/users/" + val); } } - @Test public void expandMultipleParams() { + @Test + public void expandMultipleParams() { assertThat(expand("/users/{user}/{repo}", mapOf("user", "unic???de", "repo", "foo"))) .isEqualTo("/users/unic???de/foo"); } - @Test public void expandParamKeyHyphen() { + @Test + public void expandParamKeyHyphen() { assertThat(expand("/{user-dir}", mapOf("user-dir", "foo"))) .isEqualTo("/foo"); } - @Test public void expandMissingParamProceeds() { + @Test + public void expandMissingParamProceeds() { assertThat(expand("/{user-dir}", mapOf("user_dir", "foo"))) .isEqualTo("/{user-dir}"); } - @Test public void resolveTemplateWithParameterizedPathSkipsEncodingSlash() { + @Test + public void resolveTemplateWithParameterizedPathSkipsEncodingSlash() { RequestTemplate template = new RequestTemplate().method("GET") .append("{zoneId}"); @@ -59,7 +87,8 @@ public class RequestTemplateTest { .hasUrl("/hostedzone/Z1PA6795UKMFR9"); } - @Test public void canInsertAbsoluteHref() { + @Test + public void canInsertAbsoluteHref() { RequestTemplate template = new RequestTemplate().method("GET") .append("/hostedzone/Z1PA6795UKMFR9"); @@ -69,7 +98,8 @@ public class RequestTemplateTest { .hasUrl("https://route53.amazonaws.com/2012-12-12/hostedzone/Z1PA6795UKMFR9"); } - @Test public void resolveTemplateWithBaseAndParameterizedQuery() { + @Test + public void resolveTemplateWithBaseAndParameterizedQuery() { RequestTemplate template = new RequestTemplate().method("GET") .append("/?Action=DescribeRegions").query("RegionName.1", "{region}"); @@ -82,7 +112,8 @@ public class RequestTemplateTest { ); } - @Test public void resolveTemplateWithBaseAndParameterizedIterableQuery() { + @Test + public void resolveTemplateWithBaseAndParameterizedIterableQuery() { RequestTemplate template = new RequestTemplate().method("GET") .append("/?Query=one").query("Queries", "{queries}"); @@ -95,7 +126,8 @@ public class RequestTemplateTest { ); } - @Test public void resolveTemplateWithMixedRequestLineParams() throws Exception { + @Test + public void resolveTemplateWithMixedRequestLineParams() throws Exception { RequestTemplate template = new RequestTemplate().method("GET")// .append("/domains/{domainId}/records")// .query("name", "{name}")// @@ -113,7 +145,8 @@ public class RequestTemplateTest { ); } - @Test public void insertHasQueryParams() throws Exception { + @Test + public void insertHasQueryParams() throws Exception { RequestTemplate template = new RequestTemplate().method("GET")// .append("/domains/1001/records")// .query("name", "denominator.io")// @@ -130,9 +163,11 @@ public class RequestTemplateTest { ); } - @Test public void resolveTemplateWithBodyTemplateSetsBodyAndContentLength() { + @Test + public void resolveTemplateWithBodyTemplateSetsBodyAndContentLength() { RequestTemplate template = new RequestTemplate().method("POST") - .bodyTemplate("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", " + + .bodyTemplate( + "%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", " + "\"password\": \"{password}\"%7D"); template = template.resolve( @@ -144,22 +179,24 @@ public class RequestTemplateTest { ); assertThat(template) - .hasBody("{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}") + .hasBody( + "{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}") .hasHeaders( entry("Content-Length", asList(String.valueOf(template.body().length))) ); } - @Test public void skipUnresolvedQueries() throws Exception { + @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(mapOf( - "domainId", 1001, - "nameVariable", "denominator.io" - ) + "domainId", 1001, + "nameVariable", "denominator.io" + ) ); assertThat(template) @@ -169,7 +206,8 @@ public class RequestTemplateTest { ); } - @Test public void allQueriesUnresolvable() throws Exception { + @Test + public void allQueriesUnresolvable() throws Exception { RequestTemplate template = new RequestTemplate().method("GET")// .append("/domains/{domainId}/records")// .query("optional", "{optional}")// @@ -181,23 +219,4 @@ public class RequestTemplateTest { .hasUrl("/domains/1001/records") .hasQueries(); } - - /** Avoid depending on guava solely for map literals. */ - private static Map mapOf(String key, Object val) { - Map result = new LinkedHashMap(); - result.put(key, val); - return result; - } - - private static Map mapOf(String k1, Object v1, String k2, Object v2) { - Map result = mapOf(k1, v1); - result.put(k2, v2); - return result; - } - - private static Map mapOf(String k1, Object v1, String k2, Object v2, String k3, Object v3) { - Map result = mapOf(k1, v1, k2, v2); - result.put(k3, v3); - return result; - } } diff --git a/core/src/test/java/feign/UtilTest.java b/core/src/test/java/feign/UtilTest.java index c7de8ae85a..4998cc0ac2 100644 --- a/core/src/test/java/feign/UtilTest.java +++ b/core/src/test/java/feign/UtilTest.java @@ -15,70 +15,93 @@ */ package feign; -import feign.codec.Decoder; +import org.junit.Test; + import java.io.Reader; import java.lang.reflect.Type; import java.util.List; -import org.junit.Test; + +import feign.codec.Decoder; import static feign.Util.resolveLastTypeParameter; import static org.junit.Assert.assertEquals; public class UtilTest { - interface LastTypeParameter { - final List LIST_STRING = null; - final Parameterized> PARAMETERIZED_LIST_STRING = null; - final Parameterized> PARAMETERIZED_WILDCARD_LIST_STRING = null; - final ParameterizedDecoder> PARAMETERIZED_DECODER_LIST_STRING = null; - final ParameterizedDecoder PARAMETERIZED_DECODER_UNBOUND = null; - } - - interface ParameterizedDecoder> extends Decoder { - } - - interface Parameterized { - } - - static class ParameterizedSubtype implements Parameterized { - } - - @Test public void resolveLastTypeParameterWhenNotSubtype() throws Exception { - Type context = LastTypeParameter.class.getDeclaredField("PARAMETERIZED_LIST_STRING").getGenericType(); + @Test + public void resolveLastTypeParameterWhenNotSubtype() throws Exception { + Type + context = + LastTypeParameter.class.getDeclaredField("PARAMETERIZED_LIST_STRING").getGenericType(); Type listStringType = LastTypeParameter.class.getDeclaredField("LIST_STRING").getGenericType(); Type last = resolveLastTypeParameter(context, Parameterized.class); assertEquals(listStringType, last); } - @Test public void lastTypeFromInstance() throws Exception { + @Test + public void lastTypeFromInstance() throws Exception { Parameterized instance = new ParameterizedSubtype(); Type last = resolveLastTypeParameter(instance.getClass(), Parameterized.class); assertEquals(String.class, last); } - @Test public void lastTypeFromAnonymous() throws Exception { - Parameterized instance = new Parameterized() {}; + @Test + public void lastTypeFromAnonymous() throws Exception { + Parameterized instance = new Parameterized() { + }; Type last = resolveLastTypeParameter(instance.getClass(), Parameterized.class); assertEquals(Reader.class, last); } - @Test public void resolveLastTypeParameterWhenWildcard() throws Exception { - Type context = LastTypeParameter.class.getDeclaredField("PARAMETERIZED_WILDCARD_LIST_STRING").getGenericType(); + @Test + public void resolveLastTypeParameterWhenWildcard() throws Exception { + Type + context = + LastTypeParameter.class.getDeclaredField("PARAMETERIZED_WILDCARD_LIST_STRING") + .getGenericType(); Type listStringType = LastTypeParameter.class.getDeclaredField("LIST_STRING").getGenericType(); Type last = resolveLastTypeParameter(context, Parameterized.class); assertEquals(listStringType, last); } - @Test public void resolveLastTypeParameterWhenParameterizedSubtype() throws Exception { - Type context = LastTypeParameter.class.getDeclaredField("PARAMETERIZED_DECODER_LIST_STRING").getGenericType(); + @Test + public void resolveLastTypeParameterWhenParameterizedSubtype() throws Exception { + Type + context = + LastTypeParameter.class.getDeclaredField("PARAMETERIZED_DECODER_LIST_STRING") + .getGenericType(); Type listStringType = LastTypeParameter.class.getDeclaredField("LIST_STRING").getGenericType(); Type last = resolveLastTypeParameter(context, ParameterizedDecoder.class); assertEquals(listStringType, last); } - @Test public void unboundWildcardIsObject() throws Exception { - Type context = LastTypeParameter.class.getDeclaredField("PARAMETERIZED_DECODER_UNBOUND").getGenericType(); + @Test + public void unboundWildcardIsObject() throws Exception { + Type + context = + LastTypeParameter.class.getDeclaredField("PARAMETERIZED_DECODER_UNBOUND").getGenericType(); Type last = resolveLastTypeParameter(context, ParameterizedDecoder.class); assertEquals(Object.class, last); } + + interface LastTypeParameter { + + final List LIST_STRING = null; + final Parameterized> PARAMETERIZED_LIST_STRING = null; + final Parameterized> PARAMETERIZED_WILDCARD_LIST_STRING = null; + final ParameterizedDecoder> PARAMETERIZED_DECODER_LIST_STRING = null; + final ParameterizedDecoder PARAMETERIZED_DECODER_UNBOUND = null; + } + + interface ParameterizedDecoder> extends Decoder { + + } + + interface Parameterized { + + } + + static class ParameterizedSubtype implements Parameterized { + + } } diff --git a/core/src/test/java/feign/assertj/FeignAssertions.java b/core/src/test/java/feign/assertj/FeignAssertions.java index bbd83d7c49..b0805d79c1 100644 --- a/core/src/test/java/feign/assertj/FeignAssertions.java +++ b/core/src/test/java/feign/assertj/FeignAssertions.java @@ -15,10 +15,12 @@ */ package feign.assertj; -import feign.RequestTemplate; import org.assertj.core.api.Assertions; +import feign.RequestTemplate; + public class FeignAssertions extends Assertions { + public static RequestTemplateAssert assertThat(RequestTemplate actual) { return new RequestTemplateAssert(actual); } diff --git a/core/src/test/java/feign/assertj/MockWebServerAssertions.java b/core/src/test/java/feign/assertj/MockWebServerAssertions.java index cdb354581c..ba536ce798 100644 --- a/core/src/test/java/feign/assertj/MockWebServerAssertions.java +++ b/core/src/test/java/feign/assertj/MockWebServerAssertions.java @@ -16,9 +16,11 @@ package feign.assertj; import com.squareup.okhttp.mockwebserver.RecordedRequest; + import org.assertj.core.api.Assertions; public class MockWebServerAssertions extends Assertions { + public static RecordedRequestAssert assertThat(RecordedRequest actual) { return new RecordedRequestAssert(actual); } diff --git a/core/src/test/java/feign/assertj/RecordedRequestAssert.java b/core/src/test/java/feign/assertj/RecordedRequestAssert.java index fed0d93909..bf384c187b 100644 --- a/core/src/test/java/feign/assertj/RecordedRequestAssert.java +++ b/core/src/test/java/feign/assertj/RecordedRequestAssert.java @@ -16,21 +16,26 @@ package feign.assertj; import com.squareup.okhttp.mockwebserver.RecordedRequest; -import feign.Util; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.util.LinkedHashSet; -import java.util.Set; -import java.util.zip.GZIPInputStream; + import org.assertj.core.api.AbstractAssert; import org.assertj.core.internal.ByteArrays; import org.assertj.core.internal.Failures; import org.assertj.core.internal.Iterables; import org.assertj.core.internal.Objects; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.zip.GZIPInputStream; + +import feign.Util; + import static org.assertj.core.error.ShouldNotContain.shouldNotContain; -public final class RecordedRequestAssert extends AbstractAssert { +public final class RecordedRequestAssert + extends AbstractAssert { + ByteArrays arrays = ByteArrays.instance(); Objects objects = Objects.instance(); Iterables iterables = Iterables.instance(); @@ -63,7 +68,8 @@ public RecordedRequestAssert hasGzippedBody(byte[] expectedUncompressed) { byte[] compressedBody = actual.getBody(); byte[] uncompressedBody; try { - uncompressedBody = Util.toByteArray(new GZIPInputStream(new ByteArrayInputStream(compressedBody))); + uncompressedBody = + Util.toByteArray(new GZIPInputStream(new ByteArrayInputStream(compressedBody))); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/core/src/test/java/feign/assertj/RequestTemplateAssert.java b/core/src/test/java/feign/assertj/RequestTemplateAssert.java index b2145ae777..ca18fd715a 100644 --- a/core/src/test/java/feign/assertj/RequestTemplateAssert.java +++ b/core/src/test/java/feign/assertj/RequestTemplateAssert.java @@ -15,16 +15,19 @@ */ package feign.assertj; -import feign.RequestTemplate; import org.assertj.core.api.AbstractAssert; import org.assertj.core.data.MapEntry; import org.assertj.core.internal.ByteArrays; import org.assertj.core.internal.Maps; import org.assertj.core.internal.Objects; +import feign.RequestTemplate; + import static feign.Util.UTF_8; -public final class RequestTemplateAssert extends AbstractAssert { +public final class RequestTemplateAssert + extends AbstractAssert { + ByteArrays arrays = ByteArrays.instance(); Objects objects = Objects.instance(); Maps maps = Maps.instance(); diff --git a/core/src/test/java/feign/auth/BasicAuthRequestInterceptorTest.java b/core/src/test/java/feign/auth/BasicAuthRequestInterceptorTest.java index ab332951fc..df136dd590 100644 --- a/core/src/test/java/feign/auth/BasicAuthRequestInterceptorTest.java +++ b/core/src/test/java/feign/auth/BasicAuthRequestInterceptorTest.java @@ -15,20 +15,22 @@ */ package feign.auth; -import feign.RequestTemplate; -import java.util.Collections; import org.junit.Test; +import feign.RequestTemplate; + import static feign.assertj.FeignAssertions.assertThat; import static java.util.Arrays.asList; import static org.assertj.core.data.MapEntry.entry; -import static org.junit.Assert.assertEquals; public class BasicAuthRequestInterceptorTest { - @Test public void addsAuthorizationHeader() { + @Test + public void addsAuthorizationHeader() { RequestTemplate template = new RequestTemplate(); - BasicAuthRequestInterceptor interceptor = new BasicAuthRequestInterceptor("Aladdin", "open sesame"); + BasicAuthRequestInterceptor + interceptor = + new BasicAuthRequestInterceptor("Aladdin", "open sesame"); interceptor.apply(template); assertThat(template) @@ -37,15 +39,19 @@ public class BasicAuthRequestInterceptorTest { ); } - @Test public void addsAuthorizationHeader_longUserAndPassword() { + @Test + public void addsAuthorizationHeader_longUserAndPassword() { RequestTemplate template = new RequestTemplate(); - BasicAuthRequestInterceptor interceptor = new BasicAuthRequestInterceptor("IOIOIOIOIOIOIOIOIOIOIOIOIOIOIOIOIOIOIO", - "101010101010101010101010101010101010101010"); + BasicAuthRequestInterceptor + interceptor = + new BasicAuthRequestInterceptor("IOIOIOIOIOIOIOIOIOIOIOIOIOIOIOIOIOIOIO", + "101010101010101010101010101010101010101010"); interceptor.apply(template); assertThat(template) .hasHeaders( - entry("Authorization", asList("Basic SU9JT0lPSU9JT0lPSU9JT0lPSU9JT0lPSU9JT0lPSU9JT0lPSU86MTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEw")) + entry("Authorization", asList( + "Basic SU9JT0lPSU9JT0lPSU9JT0lPSU9JT0lPSU9JT0lPSU9JT0lPSU86MTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEw")) ); } } diff --git a/core/src/test/java/feign/client/DefaultClientTest.java b/core/src/test/java/feign/client/DefaultClientTest.java index ba59b0a089..22671a3b05 100644 --- a/core/src/test/java/feign/client/DefaultClientTest.java +++ b/core/src/test/java/feign/client/DefaultClientTest.java @@ -18,20 +18,24 @@ import com.squareup.okhttp.mockwebserver.MockResponse; import com.squareup.okhttp.mockwebserver.SocketPolicy; import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.ProtocolException; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSession; + import feign.Client; import feign.Feign; import feign.FeignException; import feign.Headers; import feign.RequestLine; import feign.Response; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.net.ProtocolException; -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.SSLSession; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; import static feign.Util.UTF_8; import static feign.assertj.MockWebServerAssertions.assertThat; @@ -40,20 +44,28 @@ import static org.junit.Assert.assertEquals; public class DefaultClientTest { - @Rule public final ExpectedException thrown = ExpectedException.none(); - @Rule public final MockWebServerRule server = new MockWebServerRule(); - interface TestInterface { - @RequestLine("POST /?foo=bar&foo=baz&qux=") - @Headers({"Foo: Bar", "Foo: Baz", "Qux: ", "Content-Type: text/plain"}) Response post(String body); - - @RequestLine("PATCH /") @Headers("Accept: text/plain") String patch(); - } - - @Test public void parsesRequestAndResponse() throws IOException, InterruptedException { + @Rule + public final ExpectedException thrown = ExpectedException.none(); + @Rule + public final MockWebServerRule server = new MockWebServerRule(); + Client trustSSLSockets = new Client.Default(TrustingSSLSocketFactory.get(), null); + Client + disableHostnameVerification = + new Client.Default(TrustingSSLSocketFactory.get(), new HostnameVerifier() { + @Override + public boolean verify(String s, SSLSession sslSession) { + return true; + } + }); + + @Test + public void parsesRequestAndResponse() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("foo").addHeader("Foo: Bar")); - TestInterface api = Feign.builder().target(TestInterface.class, "http://localhost:" + server.getPort()); + TestInterface + api = + Feign.builder().target(TestInterface.class, "http://localhost:" + server.getPort()); Response response = api.post("foo"); @@ -62,7 +74,8 @@ interface TestInterface { assertThat(response.headers()) .containsEntry("Content-Length", asList("3")) .containsEntry("Foo", asList("Bar")); - assertThat(response.body().asInputStream()).hasContentEqualTo(new ByteArrayInputStream("foo".getBytes(UTF_8))); + assertThat(response.body().asInputStream()) + .hasContentEqualTo(new ByteArrayInputStream("foo".getBytes(UTF_8))); assertThat(server.takeRequest()).hasMethod("POST") .hasPath("/?foo=bar&foo=baz&qux=") @@ -70,34 +83,39 @@ interface TestInterface { .hasBody("foo"); } - @Test public void parsesErrorResponse() throws IOException, InterruptedException { + @Test + public void parsesErrorResponse() throws IOException, InterruptedException { thrown.expect(FeignException.class); thrown.expectMessage("status 500 reading TestInterface#post(String); content:\n" + "ARGHH"); server.enqueue(new MockResponse().setResponseCode(500).setBody("ARGHH")); - TestInterface api = Feign.builder().target(TestInterface.class, "http://localhost:" + server.getPort()); + TestInterface + api = + Feign.builder().target(TestInterface.class, "http://localhost:" + server.getPort()); api.post("foo"); } /** - * We currently don't include the 60-line workaround - * jersey uses to overcome the lack of support for PATCH. For now, prefer okhttp. + * We currently don't include the 60-line + * workaround jersey uses to overcome the lack of support for PATCH. For now, prefer okhttp. * * @see java.net.HttpURLConnection#setRequestMethod */ - @Test public void patchUnsupported() throws IOException, InterruptedException { + @Test + public void patchUnsupported() throws IOException, InterruptedException { thrown.expectCause(isA(ProtocolException.class)); - TestInterface api = Feign.builder().target(TestInterface.class, "http://localhost:" + server.getPort()); + TestInterface + api = + Feign.builder().target(TestInterface.class, "http://localhost:" + server.getPort()); api.patch(); } - Client trustSSLSockets = new Client.Default(TrustingSSLSocketFactory.get(), null); - - @Test public void canOverrideSSLSocketFactory() throws IOException, InterruptedException { + @Test + public void canOverrideSSLSocketFactory() throws IOException, InterruptedException { server.get().useHttps(TrustingSSLSocketFactory.get("localhost"), false); server.enqueue(new MockResponse()); @@ -108,13 +126,8 @@ interface TestInterface { api.post("foo"); } - Client disableHostnameVerification = new Client.Default(TrustingSSLSocketFactory.get(), new HostnameVerifier() { - @Override public boolean verify(String s, SSLSession sslSession) { - return true; - } - }); - - @Test public void canOverrideHostnameVerifier() throws IOException, InterruptedException { + @Test + public void canOverrideHostnameVerifier() throws IOException, InterruptedException { server.get().useHttps(TrustingSSLSocketFactory.get("bad.example.com"), false); server.enqueue(new MockResponse()); @@ -125,7 +138,8 @@ interface TestInterface { api.post("foo"); } - @Test public void retriesFailedHandshake() throws IOException, InterruptedException { + @Test + public void retriesFailedHandshake() throws IOException, InterruptedException { server.get().useHttps(TrustingSSLSocketFactory.get("localhost"), false); server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE)); server.enqueue(new MockResponse()); @@ -137,4 +151,15 @@ interface TestInterface { api.post("foo"); assertEquals(2, server.getRequestCount()); } + + interface TestInterface { + + @RequestLine("POST /?foo=bar&foo=baz&qux=") + @Headers({"Foo: Bar", "Foo: Baz", "Qux: ", "Content-Type: text/plain"}) + Response post(String body); + + @RequestLine("PATCH /") + @Headers("Accept: text/plain") + String patch(); + } } diff --git a/core/src/test/java/feign/client/TrustingSSLSocketFactory.java b/core/src/test/java/feign/client/TrustingSSLSocketFactory.java index b67225bbba..aa15be208a 100644 --- a/core/src/test/java/feign/client/TrustingSSLSocketFactory.java +++ b/core/src/test/java/feign/client/TrustingSSLSocketFactory.java @@ -28,6 +28,7 @@ import java.util.Arrays; import java.util.LinkedHashMap; import java.util.Map; + import javax.net.ssl.KeyManager; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; @@ -39,28 +40,18 @@ /** * Used for ssl tests to simplify setup. */ -final class TrustingSSLSocketFactory extends SSLSocketFactory implements X509TrustManager, X509KeyManager { - - private static final Map sslSocketFactories = new LinkedHashMap(); - - public static SSLSocketFactory get() { - return get(""); - } - - public synchronized static SSLSocketFactory get(String serverAlias) { - if (!sslSocketFactories.containsKey(serverAlias)) { - sslSocketFactories.put(serverAlias, new TrustingSSLSocketFactory(serverAlias)); - } - return sslSocketFactories.get(serverAlias); - } +final class TrustingSSLSocketFactory extends SSLSocketFactory + implements X509TrustManager, X509KeyManager { + private static final Map + sslSocketFactories = + new LinkedHashMap(); private static final char[] KEYSTORE_PASSWORD = "password".toCharArray(); - + private final static String[] ENABLED_CIPHER_SUITES = {"SSL_RSA_WITH_RC4_128_MD5"}; 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"); @@ -75,7 +66,9 @@ private TrustingSSLSocketFactory(String serverAlias) { this.certificateChain = null; } else { try { - KeyStore keyStore = loadKeyStore(TrustingSSLSocketFactory.class.getResourceAsStream("/keystore.jks")); + KeyStore + keyStore = + loadKeyStore(TrustingSSLSocketFactory.class.getResourceAsStream("/keystore.jks")); this.privateKey = (PrivateKey) keyStore.getKey(serverAlias, KEYSTORE_PASSWORD); Certificate[] rawChain = keyStore.getCertificateChain(serverAlias); this.certificateChain = Arrays.copyOf(rawChain, rawChain.length, X509Certificate[].class); @@ -85,22 +78,48 @@ private TrustingSSLSocketFactory(String serverAlias) { } } - @Override public String[] getDefaultCipherSuites() { - return ENABLED_CIPHER_SUITES; + public static SSLSocketFactory get() { + return get(""); + } + + public synchronized static SSLSocketFactory get(String serverAlias) { + if (!sslSocketFactories.containsKey(serverAlias)) { + sslSocketFactories.put(serverAlias, new TrustingSSLSocketFactory(serverAlias)); + } + return sslSocketFactories.get(serverAlias); + } + + static Socket setEnabledCipherSuites(Socket socket) { + SSLSocket.class.cast(socket).setEnabledCipherSuites(ENABLED_CIPHER_SUITES); + return socket; + } + + private static KeyStore loadKeyStore(InputStream inputStream) throws IOException { + try { + KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(inputStream, KEYSTORE_PASSWORD); + return keyStore; + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + inputStream.close(); + } } - @Override public String[] getSupportedCipherSuites() { + @Override + public String[] getDefaultCipherSuites() { 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)); + public String[] getSupportedCipherSuites() { + return ENABLED_CIPHER_SUITES; } - static Socket setEnabledCipherSuites(Socket socket) { - SSLSocket.class.cast(socket).setEnabledCipherSuites(ENABLED_CIPHER_SUITES); - return socket; + @Override + public Socket createSocket(Socket s, String host, int port, boolean autoClose) + throws IOException { + return setEnabledCipherSuites(delegate.createSocket(s, host, port, autoClose)); } @Override @@ -108,12 +127,14 @@ 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 { + @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 { + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) + throws IOException { return setEnabledCipherSuites(delegate.createSocket(host, port, localHost, localPort)); } @@ -162,18 +183,4 @@ public X509Certificate[] getCertificateChain(String alias) { public PrivateKey getPrivateKey(String alias) { return privateKey; } - - private static KeyStore loadKeyStore(InputStream inputStream) throws IOException { - try { - KeyStore keyStore = KeyStore.getInstance("JKS"); - keyStore.load(inputStream, KEYSTORE_PASSWORD); - return keyStore; - } catch (Exception e) { - throw new RuntimeException(e); - } finally { - inputStream.close(); - } - } - - private final static String[] ENABLED_CIPHER_SUITES = {"SSL_RSA_WITH_RC4_128_MD5"}; } diff --git a/core/src/test/java/feign/codec/DefaultDecoderTest.java b/core/src/test/java/feign/codec/DefaultDecoderTest.java index 02c86c167f..5bfffd4708 100644 --- a/core/src/test/java/feign/codec/DefaultDecoderTest.java +++ b/core/src/test/java/feign/codec/DefaultDecoderTest.java @@ -15,46 +15,54 @@ */ package feign.codec; -import feign.Response; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.w3c.dom.Document; + import java.io.ByteArrayInputStream; import java.io.InputStream; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.w3c.dom.Document; + +import feign.Response; import static feign.Util.UTF_8; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; public class DefaultDecoderTest { - @Rule public final ExpectedException thrown = ExpectedException.none(); + + @Rule + public final ExpectedException thrown = ExpectedException.none(); private final Decoder decoder = new Decoder.Default(); - @Test public void testDecodesToString() throws Exception { + @Test + public void testDecodesToString() throws Exception { Response response = knownResponse(); Object decodedObject = decoder.decode(response, String.class); assertEquals(String.class, decodedObject.getClass()); assertEquals("response body", decodedObject.toString()); } - @Test public void testDecodesToByteArray() throws Exception { + @Test + public void testDecodesToByteArray() throws Exception { Response response = knownResponse(); Object decodedObject = decoder.decode(response, byte[].class); assertEquals(byte[].class, decodedObject.getClass()); assertEquals("response body", new String((byte[]) decodedObject, UTF_8)); } - @Test public void testDecodesNullBodyToNull() throws Exception { + @Test + public void testDecodesNullBodyToNull() throws Exception { assertNull(decoder.decode(nullBodyResponse(), Document.class)); } - @Test public void testRefusesToDecodeOtherTypes() throws Exception { + @Test + public void testRefusesToDecodeOtherTypes() throws Exception { thrown.expect(DecodeException.class); thrown.expectMessage(" is not a type supported by this decoder."); @@ -70,6 +78,7 @@ private Response knownResponse() { } private Response nullBodyResponse() { - return Response.create(200, "OK", Collections.>emptyMap(), (byte[]) null); + return Response + .create(200, "OK", Collections.>emptyMap(), (byte[]) null); } } diff --git a/core/src/test/java/feign/codec/DefaultEncoderTest.java b/core/src/test/java/feign/codec/DefaultEncoderTest.java index 71d3367491..70e17602e1 100644 --- a/core/src/test/java/feign/codec/DefaultEncoderTest.java +++ b/core/src/test/java/feign/codec/DefaultEncoderTest.java @@ -15,37 +15,44 @@ */ package feign.codec; -import feign.RequestTemplate; -import java.util.Arrays; -import java.util.Date; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import java.util.Arrays; +import java.util.Date; + +import feign.RequestTemplate; + import static feign.Util.UTF_8; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; public class DefaultEncoderTest { - @Rule public final ExpectedException thrown = ExpectedException.none(); + + @Rule + public final ExpectedException thrown = ExpectedException.none(); private final Encoder encoder = new Encoder.Default(); - @Test public void testEncodesStrings() throws Exception { + @Test + public void testEncodesStrings() throws Exception { String content = "This is my content"; RequestTemplate template = new RequestTemplate(); encoder.encode(content, String.class, template); assertEquals(content, new String(template.body(), UTF_8)); } - @Test public void testEncodesByteArray() throws Exception { + @Test + public void testEncodesByteArray() throws Exception { byte[] content = {12, 34, 56}; RequestTemplate template = new RequestTemplate(); encoder.encode(content, byte[].class, template); assertTrue(Arrays.equals(content, template.body())); } - @Test public void testRefusesToEncodeOtherTypes() throws Exception { + @Test + public void testRefusesToEncodeOtherTypes() throws Exception { thrown.expect(EncodeException.class); thrown.expectMessage("is not a type supported by this encoder."); diff --git a/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java b/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java index 1fd443feee..bd49984e54 100644 --- a/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java +++ b/core/src/test/java/feign/codec/DefaultErrorDecoderTest.java @@ -15,27 +15,32 @@ */ package feign.codec; -import feign.FeignException; -import feign.Response; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + import java.util.Arrays; import java.util.Collection; import java.util.LinkedHashMap; import java.util.Map; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; + +import feign.FeignException; +import feign.Response; import static feign.Util.RETRY_AFTER; import static feign.Util.UTF_8; public class DefaultErrorDecoderTest { - @Rule public final ExpectedException thrown = ExpectedException.none(); + + @Rule + public final ExpectedException thrown = ExpectedException.none(); ErrorDecoder errorDecoder = new ErrorDecoder.Default(); Map> headers = new LinkedHashMap>(); - @Test public void throwsFeignException() throws Throwable { + @Test + public void throwsFeignException() throws Throwable { thrown.expect(FeignException.class); thrown.expectMessage("status 500 reading Service#foo()"); @@ -44,16 +49,20 @@ public class DefaultErrorDecoderTest { throw errorDecoder.decode("Service#foo()", response); } - @Test public void throwsFeignExceptionIncludingBody() throws Throwable { + @Test + public void throwsFeignExceptionIncludingBody() throws Throwable { thrown.expect(FeignException.class); thrown.expectMessage("status 500 reading Service#foo(); content:\nhello world"); - Response response = Response.create(500, "Internal server error", headers, "hello world", UTF_8); + Response + response = + Response.create(500, "Internal server error", headers, "hello world", UTF_8); throw errorDecoder.decode("Service#foo()", response); } - @Test public void retryAfterHeaderThrowsRetryableException() throws Throwable { + @Test + public void retryAfterHeaderThrowsRetryableException() throws Throwable { thrown.expect(FeignException.class); thrown.expectMessage("status 503 reading Service#foo()"); diff --git a/core/src/test/java/feign/codec/RetryAfterDecoderTest.java b/core/src/test/java/feign/codec/RetryAfterDecoderTest.java index 06ba5496cc..d7aef4fd8f 100644 --- a/core/src/test/java/feign/codec/RetryAfterDecoderTest.java +++ b/core/src/test/java/feign/codec/RetryAfterDecoderTest.java @@ -15,10 +15,12 @@ */ package feign.codec; -import feign.codec.ErrorDecoder.RetryAfterDecoder; -import java.text.ParseException; import org.junit.Test; +import java.text.ParseException; + +import feign.codec.ErrorDecoder.RetryAfterDecoder; + import static feign.codec.ErrorDecoder.RetryAfterDecoder.RFC822_FORMAT; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.junit.Assert.assertEquals; @@ -26,19 +28,6 @@ public class RetryAfterDecoderTest { - @Test public void malformDateFailsGracefully() { - assertFalse(decoder.apply("Fri, 31 Dec 1999 23:59:59 ZBW") != null); - } - - @Test public void rfc822Parses() throws ParseException { - assertEquals(RFC822_FORMAT.parse("Fri, 31 Dec 1999 23:59:59 GMT"), - decoder.apply("Fri, 31 Dec 1999 23:59:59 GMT")); - } - - @Test public void relativeSecondsParses() throws ParseException { - assertEquals(RFC822_FORMAT.parse("Sun, 2 Jan 2000 00:00:00 GMT"), decoder.apply("86400")); - } - private RetryAfterDecoder decoder = new RetryAfterDecoder(RFC822_FORMAT) { protected long currentTimeNanos() { try { @@ -48,4 +37,20 @@ protected long currentTimeNanos() { } } }; + + @Test + public void malformDateFailsGracefully() { + assertFalse(decoder.apply("Fri, 31 Dec 1999 23:59:59 ZBW") != null); + } + + @Test + public void rfc822Parses() throws ParseException { + assertEquals(RFC822_FORMAT.parse("Fri, 31 Dec 1999 23:59:59 GMT"), + decoder.apply("Fri, 31 Dec 1999 23:59:59 GMT")); + } + + @Test + public void relativeSecondsParses() throws ParseException { + assertEquals(RFC822_FORMAT.parse("Sun, 2 Jan 2000 00:00:00 GMT"), decoder.apply("86400")); + } } diff --git a/core/src/test/java/feign/examples/GitHubExample.java b/core/src/test/java/feign/examples/GitHubExample.java index 71d7b04ff4..ae41a8f677 100644 --- a/core/src/test/java/feign/examples/GitHubExample.java +++ b/core/src/test/java/feign/examples/GitHubExample.java @@ -17,6 +17,12 @@ import com.google.gson.Gson; import com.google.gson.JsonIOException; + +import java.io.IOException; +import java.io.Reader; +import java.lang.reflect.Type; +import java.util.List; + import feign.Feign; import feign.Logger; import feign.Param; @@ -24,11 +30,6 @@ import feign.Response; import feign.codec.Decoder; -import java.io.IOException; -import java.io.Reader; -import java.lang.reflect.Type; -import java.util.List; - import static feign.Util.ensureClosed; /** @@ -36,22 +37,12 @@ */ public class GitHubExample { - interface GitHub { - @RequestLine("GET /repos/{owner}/{repo}/contributors") - List contributors(@Param("owner") String owner, @Param("repo") String repo); - } - - static class Contributor { - String login; - int contributions; - } - public static void main(String... args) { GitHub github = Feign.builder() - .decoder(new GsonDecoder()) - .logger(new Logger.ErrorLogger()) - .logLevel(Logger.Level.BASIC) - .target(GitHub.class, "https://api.github.com"); + .decoder(new GsonDecoder()) + .logger(new Logger.ErrorLogger()) + .logLevel(Logger.Level.BASIC) + .target(GitHub.class, "https://api.github.com"); System.out.println("Let's fetch and print a list of the contributors to this library."); List contributors = github.contributors("netflix", "feign"); @@ -60,13 +51,27 @@ public static void main(String... args) { } } + interface GitHub { + + @RequestLine("GET /repos/{owner}/{repo}/contributors") + List contributors(@Param("owner") String owner, @Param("repo") String repo); + } + + static class Contributor { + + String login; + int contributions; + } + /** * Here's how it looks to write a decoder. Note: you can instead use {@code feign-gson}! */ static class GsonDecoder implements Decoder { + private final Gson gson = new Gson(); - @Override public Object decode(Response response, Type type) throws IOException { + @Override + public Object decode(Response response, Type type) throws IOException { if (void.class == type || response.body() == null) { return null; } diff --git a/example-github/src/main/java/feign/example/github/GitHubExample.java b/example-github/src/main/java/feign/example/github/GitHubExample.java index f1054f4cae..cca535e90d 100644 --- a/example-github/src/main/java/feign/example/github/GitHubExample.java +++ b/example-github/src/main/java/feign/example/github/GitHubExample.java @@ -15,28 +15,19 @@ */ package feign.example.github; +import java.util.List; + import feign.Feign; import feign.Logger; import feign.Param; import feign.RequestLine; import feign.gson.GsonDecoder; -import java.util.List; /** * adapted from {@code com.example.retrofit.GitHubClient} */ public class GitHubExample { - interface GitHub { - @RequestLine("GET /repos/{owner}/{repo}/contributors") - List contributors(@Param("owner") String owner, @Param("repo") String repo); - } - - static class Contributor { - String login; - int contributions; - } - public static void main(String... args) throws InterruptedException { GitHub github = Feign.builder() .decoder(new GsonDecoder()) @@ -50,4 +41,16 @@ public static void main(String... args) throws InterruptedException { System.out.println(contributor.login + " (" + contributor.contributions + ")"); } } + + interface GitHub { + + @RequestLine("GET /repos/{owner}/{repo}/contributors") + List contributors(@Param("owner") String owner, @Param("repo") String repo); + } + + static class Contributor { + + String login; + int contributions; + } } diff --git a/example-wikipedia/src/main/java/feign/example/wikipedia/ResponseAdapter.java b/example-wikipedia/src/main/java/feign/example/wikipedia/ResponseAdapter.java index 3c5d77c2e2..c7c243622d 100644 --- a/example-wikipedia/src/main/java/feign/example/wikipedia/ResponseAdapter.java +++ b/example-wikipedia/src/main/java/feign/example/wikipedia/ResponseAdapter.java @@ -9,17 +9,15 @@ abstract class ResponseAdapter extends TypeAdapter> { /** - * name of the key inside the {@code query} dict which holds the elements desired. ex. {@code pages}. + * 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. - *

- *
- * 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}. - *

+ * Parses the contents of a result object.


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}.

*

    * "pages": {
    *   "2576129": {
diff --git a/example-wikipedia/src/main/java/feign/example/wikipedia/WikipediaExample.java b/example-wikipedia/src/main/java/feign/example/wikipedia/WikipediaExample.java
index bdaad34ffa..dabc7e799b 100644
--- a/example-wikipedia/src/main/java/feign/example/wikipedia/WikipediaExample.java
+++ b/example-wikipedia/src/main/java/feign/example/wikipedia/WikipediaExample.java
@@ -19,38 +19,47 @@
 import com.google.gson.GsonBuilder;
 import com.google.gson.reflect.TypeToken;
 import com.google.gson.stream.JsonReader;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+
 import feign.Feign;
 import feign.Logger;
 import feign.Param;
 import feign.RequestLine;
 import feign.gson.GsonDecoder;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Iterator;
 
 public class WikipediaExample {
 
-  public static interface Wikipedia {
-    @RequestLine("GET /w/api.php?action=query&continue=&generator=search&prop=info&format=json&gsrsearch={search}")
-    Response search(@Param("search") String search);
-
-    @RequestLine("GET /w/api.php?action=query&continue=&generator=search&prop=info&format=json&gsrsearch={search}&gsroffset={offset}")
-    Response resumeSearch(@Param("search") String search, @Param("offset") long offset);
-  }
+  static ResponseAdapter pagesAdapter = new ResponseAdapter() {
 
-  static class Page {
-    long id;
-    String title;
-  }
+    @Override
+    protected String query() {
+      return "pages";
+    }
 
-  public static class Response extends ArrayList {
-    /** when present, the position to resume the list. */
-    Long nextOffset;
-  }
+    @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;
+    }
+  };
 
   public static void main(String... args) throws InterruptedException {
     Gson gson = new GsonBuilder()
-        .registerTypeAdapter(new TypeToken>(){}.getType(), pagesAdapter)
+        .registerTypeAdapter(new TypeToken>() {
+        }.getType(), pagesAdapter)
         .create();
 
     Wikipedia wikipedia = Feign.builder()
@@ -74,8 +83,9 @@ public static void main(String... args) throws InterruptedException {
    */
   static Iterator lazySearch(final Wikipedia wikipedia, final String query) {
     final Response first = wikipedia.search(query);
-    if (first.nextOffset == null)
+    if (first.nextOffset == null) {
       return first.iterator();
+    }
     return new Iterator() {
       Iterator current = first.iterator();
       Long nextOffset = first.nextOffset;
@@ -103,25 +113,26 @@ public void remove() {
     };
   }
 
-  static ResponseAdapter pagesAdapter = new ResponseAdapter() {
+  public static interface Wikipedia {
 
-    @Override protected String query() {
-      return "pages";
-    }
+    @RequestLine("GET /w/api.php?action=query&continue=&generator=search&prop=info&format=json&gsrsearch={search}")
+    Response search(@Param("search") String search);
 
-    @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;
-    }
-  };
+    @RequestLine("GET /w/api.php?action=query&continue=&generator=search&prop=info&format=json&gsrsearch={search}&gsroffset={offset}")
+    Response resumeSearch(@Param("search") String search, @Param("offset") long offset);
+  }
+
+  static class Page {
+
+    long id;
+    String title;
+  }
+
+  public static class Response extends ArrayList {
+
+    /**
+     * when present, the position to resume the list.
+     */
+    Long nextOffset;
+  }
 }
diff --git a/gson/src/main/java/feign/gson/DoubleToIntMapTypeAdapter.java b/gson/src/main/java/feign/gson/DoubleToIntMapTypeAdapter.java
index 3a92f4f8a5..9de868998f 100644
--- a/gson/src/main/java/feign/gson/DoubleToIntMapTypeAdapter.java
+++ b/gson/src/main/java/feign/gson/DoubleToIntMapTypeAdapter.java
@@ -33,16 +33,22 @@
  * Deals with scenario where Gson Object type treats all numbers as doubles.
  */
 public class DoubleToIntMapTypeAdapter extends TypeAdapter> {
-  final static TypeToken> token = new TypeToken>() {};
 
-  private final TypeAdapter> delegate = new MapTypeAdapterFactory(new ConstructorConstructor(
-      Collections.>emptyMap()), false).create(new Gson(), token);
+  final static TypeToken> token = new TypeToken>() {
+  };
 
-  @Override public void write(JsonWriter out, Map value) throws IOException {
+  private final TypeAdapter>
+      delegate =
+      new MapTypeAdapterFactory(new ConstructorConstructor(
+          Collections.>emptyMap()), false).create(new Gson(), token);
+
+  @Override
+  public void write(JsonWriter out, Map value) throws IOException {
     delegate.write(out, value);
   }
 
-  @Override public Map read(JsonReader in) throws IOException {
+  @Override
+  public Map read(JsonReader in) throws IOException {
     Map map = delegate.read(in);
     for (Map.Entry entry : map.entrySet()) {
       if (entry.getValue() instanceof Double) {
diff --git a/gson/src/main/java/feign/gson/GsonDecoder.java b/gson/src/main/java/feign/gson/GsonDecoder.java
index 0a01cc959c..cd5fcf4b2f 100644
--- a/gson/src/main/java/feign/gson/GsonDecoder.java
+++ b/gson/src/main/java/feign/gson/GsonDecoder.java
@@ -18,16 +18,19 @@
 import com.google.gson.Gson;
 import com.google.gson.JsonIOException;
 import com.google.gson.TypeAdapter;
-import feign.Response;
-import feign.codec.Decoder;
+
 import java.io.IOException;
 import java.io.Reader;
 import java.lang.reflect.Type;
 import java.util.Collections;
 
+import feign.Response;
+import feign.codec.Decoder;
+
 import static feign.Util.ensureClosed;
 
 public class GsonDecoder implements Decoder {
+
   private final Gson gson;
 
   public GsonDecoder(Iterable> adapters) {
@@ -42,7 +45,8 @@ public GsonDecoder(Gson gson) {
     this.gson = gson;
   }
 
-  @Override public Object decode(Response response, Type type) throws IOException {
+  @Override
+  public Object decode(Response response, Type type) throws IOException {
     if (response.body() == null) {
       return null;
     }
diff --git a/gson/src/main/java/feign/gson/GsonEncoder.java b/gson/src/main/java/feign/gson/GsonEncoder.java
index a01772b6f2..5c00177660 100644
--- a/gson/src/main/java/feign/gson/GsonEncoder.java
+++ b/gson/src/main/java/feign/gson/GsonEncoder.java
@@ -17,18 +17,21 @@
 
 import com.google.gson.Gson;
 import com.google.gson.TypeAdapter;
-import feign.RequestTemplate;
-import feign.codec.Encoder;
+
 import java.lang.reflect.Type;
 import java.util.Collections;
 
+import feign.RequestTemplate;
+import feign.codec.Encoder;
+
 public class GsonEncoder implements Encoder {
+
   private final Gson gson;
 
   public GsonEncoder(Iterable> adapters) {
     this(GsonFactory.create(adapters));
   }
-  
+
   public GsonEncoder() {
     this(Collections.>emptyList());
   }
@@ -37,7 +40,8 @@ public GsonEncoder(Gson gson) {
     this.gson = gson;
   }
 
-  @Override public void encode(Object object, Type bodyType, RequestTemplate template) {
+  @Override
+  public void encode(Object object, Type bodyType, RequestTemplate template) {
     template.body(gson.toJson(object, bodyType));
   }
 }
diff --git a/gson/src/main/java/feign/gson/GsonFactory.java b/gson/src/main/java/feign/gson/GsonFactory.java
index 7685b96b28..ca6b428a3f 100644
--- a/gson/src/main/java/feign/gson/GsonFactory.java
+++ b/gson/src/main/java/feign/gson/GsonFactory.java
@@ -19,6 +19,7 @@
 import com.google.gson.GsonBuilder;
 import com.google.gson.TypeAdapter;
 import com.google.gson.reflect.TypeToken;
+
 import java.lang.reflect.Type;
 import java.util.Map;
 
@@ -26,9 +27,12 @@
 
 final class GsonFactory {
 
+  private GsonFactory() {
+  }
+
   /**
-   * Registers type adapters by implicit type. Adds one to read numbers in a 
-   * {@code Map} as Integers.
+   * Registers type adapters by implicit type. Adds one to read numbers in a {@code Map} as Integers.
    */
   static Gson create(Iterable> adapters) {
     GsonBuilder builder = new GsonBuilder().setPrettyPrinting();
@@ -40,7 +44,4 @@ static Gson create(Iterable> adapters) {
     }
     return builder.create();
   }
-
-  private GsonFactory() {
-  }
 }
diff --git a/gson/src/test/java/feign/gson/GsonCodecTest.java b/gson/src/test/java/feign/gson/GsonCodecTest.java
index c3b1ba3b75..ff68256f52 100644
--- a/gson/src/test/java/feign/gson/GsonCodecTest.java
+++ b/gson/src/test/java/feign/gson/GsonCodecTest.java
@@ -19,8 +19,9 @@
 import com.google.gson.reflect.TypeToken;
 import com.google.gson.stream.JsonReader;
 import com.google.gson.stream.JsonWriter;
-import feign.RequestTemplate;
-import feign.Response;
+
+import org.junit.Test;
+
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.Collection;
@@ -29,7 +30,9 @@
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
-import org.junit.Test;
+
+import feign.RequestTemplate;
+import feign.Response;
 
 import static feign.Util.UTF_8;
 import static feign.assertj.FeignAssertions.assertThat;
@@ -38,7 +41,8 @@
 
 public class GsonCodecTest {
 
-  @Test public void encodesMapObjectNumericalValuesAsInteger() throws Exception {
+  @Test
+  public void encodesMapObjectNumericalValuesAsInteger() throws Exception {
     Map map = new LinkedHashMap();
     map.put("foo", 1);
 
@@ -46,41 +50,46 @@ public class GsonCodecTest {
     new GsonEncoder().encode(map, map.getClass(), template);
 
     assertThat(template).hasBody("" //
-            + "{\n" //
-            + "  \"foo\": 1\n" //
-            + "}");
+                                 + "{\n" //
+                                 + "  \"foo\": 1\n" //
+                                 + "}");
   }
 
-  @Test public void decodesMapObjectNumericalValuesAsInteger() throws Exception {
+  @Test
+  public void decodesMapObjectNumericalValuesAsInteger() throws Exception {
     Map map = new LinkedHashMap();
     map.put("foo", 1);
 
     Response response =
-        Response.create(200, "OK", Collections.>emptyMap(), "{\"foo\": 1}", UTF_8);
+        Response.create(200, "OK", Collections.>emptyMap(),
+                        "{\"foo\": 1}", UTF_8);
     assertEquals(new GsonDecoder().decode(response, new TypeToken>() {
     }.getType()), map);
   }
 
-  @Test public void encodesFormParams() throws Exception {
+  @Test
+  public void encodesFormParams() throws Exception {
 
     Map form = new LinkedHashMap();
     form.put("foo", 1);
     form.put("bar", Arrays.asList(2, 3));
 
     RequestTemplate template = new RequestTemplate();
-    new GsonEncoder().encode(form, new TypeToken>(){}.getType(), template);
+    new GsonEncoder().encode(form, new TypeToken>() {
+    }.getType(), template);
 
     assertThat(template).hasBody("" // 
-        + "{\n" //
-        + "  \"foo\": 1,\n" //
-        + "  \"bar\": [\n" //
-        + "    2,\n" //
-        + "    3\n" //
-        + "  ]\n" //
-        + "}");
+                                 + "{\n" //
+                                 + "  \"foo\": 1,\n" //
+                                 + "  \"bar\": [\n" //
+                                 + "    2,\n" //
+                                 + "    3\n" //
+                                 + "  ]\n" //
+                                 + "}");
   }
 
   static class Zone extends LinkedHashMap {
+
     Zone() {
       // for reflective instantiation.
     }
@@ -91,52 +100,60 @@ static class Zone extends LinkedHashMap {
 
     Zone(String name, String id) {
       put("name", name);
-      if (id != null)
+      if (id != null) {
         put("id", id);
+      }
     }
 
     private static final long serialVersionUID = 1L;
   }
 
-  @Test public void decodes() throws Exception {
+  @Test
+  public void decodes() throws Exception {
 
     List zones = new LinkedList();
     zones.add(new Zone("denominator.io."));
     zones.add(new Zone("denominator.io.", "ABCD"));
 
     Response response =
-        Response.create(200, "OK", Collections.>emptyMap(), zonesJson, UTF_8);
+        Response.create(200, "OK", Collections.>emptyMap(), zonesJson,
+                        UTF_8);
     assertEquals(zones, new GsonDecoder().decode(response, new TypeToken>() {
     }.getType()));
   }
 
-  @Test public void nullBodyDecodesToNull() throws Exception {
-    Response response = Response.create(204, "OK", Collections.>emptyMap(), (byte[]) null);
+  @Test
+  public void nullBodyDecodesToNull() throws Exception {
+    Response response = Response.create(204, "OK",
+                                        Collections.>emptyMap(),
+                                        (byte[]) null);
     assertNull(new GsonDecoder().decode(response, String.class));
   }
 
   private String zonesJson = ""//
-      + "[\n"//
-      + "  {\n"//
-      + "    \"name\": \"denominator.io.\"\n"//
-      + "  },\n"//
-      + "  {\n"//
-      + "    \"name\": \"denominator.io.\",\n"//
-      + "    \"id\": \"ABCD\"\n"//
-      + "  }\n"//
-      + "]\n";
+                             + "[\n"//
+                             + "  {\n"//
+                             + "    \"name\": \"denominator.io.\"\n"//
+                             + "  },\n"//
+                             + "  {\n"//
+                             + "    \"name\": \"denominator.io.\",\n"//
+                             + "    \"id\": \"ABCD\"\n"//
+                             + "  }\n"//
+                             + "]\n";
 
   final TypeAdapter upperZone = new TypeAdapter() {
 
-    @Override public void write(JsonWriter out, Zone value) throws IOException {
+    @Override
+    public void write(JsonWriter out, Zone value) throws IOException {
       out.beginObject();
-      for(Map.Entry entry : value.entrySet()) {
+      for (Map.Entry entry : value.entrySet()) {
         out.name(entry.getKey()).value(entry.getValue().toString().toUpperCase());
       }
       out.endObject();
     }
 
-    @Override public Zone read(JsonReader in) throws IOException {
+    @Override
+    public Zone read(JsonReader in) throws IOException {
       in.beginObject();
       Zone zone = new Zone();
       while (in.hasNext()) {
@@ -147,7 +164,8 @@ static class Zone extends LinkedHashMap {
     }
   };
 
-  @Test public void customDecoder() throws Exception {
+  @Test
+  public void customDecoder() throws Exception {
     GsonDecoder decoder = new GsonDecoder(Arrays.>asList(upperZone));
 
     List zones = new LinkedList();
@@ -155,12 +173,14 @@ static class Zone extends LinkedHashMap {
     zones.add(new Zone("DENOMINATOR.IO.", "ABCD"));
 
     Response response =
-        Response.create(200, "OK", Collections.>emptyMap(), zonesJson, UTF_8);
+        Response.create(200, "OK", Collections.>emptyMap(), zonesJson,
+                        UTF_8);
     assertEquals(zones, decoder.decode(response, new TypeToken>() {
     }.getType()));
   }
 
-  @Test public void customEncoder() throws Exception {
+  @Test
+  public void customEncoder() throws Exception {
     GsonEncoder encoder = new GsonEncoder(Arrays.>asList(upperZone));
 
     List zones = new LinkedList();
@@ -168,17 +188,18 @@ static class Zone extends LinkedHashMap {
     zones.add(new Zone("denominator.io.", "abcd"));
 
     RequestTemplate template = new RequestTemplate();
-    encoder.encode(zones, new TypeToken>(){}.getType(), template);
+    encoder.encode(zones, new TypeToken>() {
+    }.getType(), template);
 
     assertThat(template).hasBody("" //
-        + "[\n" //
-        + "  {\n" //
-        + "    \"name\": \"DENOMINATOR.IO.\"\n" //
-        + "  },\n" //
-        + "  {\n" //
-        + "    \"name\": \"DENOMINATOR.IO.\",\n" //
-        + "    \"id\": \"ABCD\"\n" //
-        + "  }\n" //
-        + "]");
+                                 + "[\n" //
+                                 + "  {\n" //
+                                 + "    \"name\": \"DENOMINATOR.IO.\"\n" //
+                                 + "  },\n" //
+                                 + "  {\n" //
+                                 + "    \"name\": \"DENOMINATOR.IO.\",\n" //
+                                 + "    \"id\": \"ABCD\"\n" //
+                                 + "  }\n" //
+                                 + "]");
   }
 }
diff --git a/gson/src/test/java/feign/gson/examples/GitHubExample.java b/gson/src/test/java/feign/gson/examples/GitHubExample.java
index 7526bdffb6..5d021b61cb 100644
--- a/gson/src/test/java/feign/gson/examples/GitHubExample.java
+++ b/gson/src/test/java/feign/gson/examples/GitHubExample.java
@@ -15,31 +15,22 @@
  */
 package feign.gson.examples;
 
+import java.util.List;
+
 import feign.Feign;
 import feign.Param;
 import feign.RequestLine;
 import feign.gson.GsonDecoder;
-import java.util.List;
 
 /**
  * adapted from {@code com.example.retrofit.GitHubClient}
  */
 public class GitHubExample {
 
-  interface GitHub {
-    @RequestLine("GET /repos/{owner}/{repo}/contributors")
-    List contributors(@Param("owner") String owner, @Param("repo") String repo);
-  }
-
-  static class Contributor {
-    String login;
-    int contributions;
-  }
-
   public static void main(String... args) {
     GitHub github = Feign.builder()
-                         .decoder(new GsonDecoder())
-                         .target(GitHub.class, "https://api.github.com");
+        .decoder(new GsonDecoder())
+        .target(GitHub.class, "https://api.github.com");
 
     System.out.println("Let's fetch and print a list of the contributors to this library.");
     List contributors = github.contributors("netflix", "feign");
@@ -47,4 +38,16 @@ public static void main(String... args) {
       System.out.println(contributor.login + " (" + contributor.contributions + ")");
     }
   }
+
+  interface GitHub {
+
+    @RequestLine("GET /repos/{owner}/{repo}/contributors")
+    List contributors(@Param("owner") String owner, @Param("repo") String repo);
+  }
+
+  static class Contributor {
+
+    String login;
+    int contributions;
+  }
 }
diff --git a/jackson/src/main/java/feign/jackson/JacksonDecoder.java b/jackson/src/main/java/feign/jackson/JacksonDecoder.java
index ffeabce5b6..4e8bdbc8f6 100644
--- a/jackson/src/main/java/feign/jackson/JacksonDecoder.java
+++ b/jackson/src/main/java/feign/jackson/JacksonDecoder.java
@@ -19,15 +19,17 @@
 import com.fasterxml.jackson.databind.Module;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.RuntimeJsonMappingException;
-import feign.Response;
-import feign.codec.Decoder;
 
 import java.io.IOException;
 import java.io.InputStream;
 import java.lang.reflect.Type;
 import java.util.Collections;
 
+import feign.Response;
+import feign.codec.Decoder;
+
 public class JacksonDecoder implements Decoder {
+
   private final ObjectMapper mapper;
 
   public JacksonDecoder() {
@@ -36,14 +38,15 @@ public JacksonDecoder() {
 
   public JacksonDecoder(Iterable modules) {
     this(new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
-        .registerModules(modules));
+             .registerModules(modules));
   }
 
   public JacksonDecoder(ObjectMapper mapper) {
     this.mapper = mapper;
   }
 
-  @Override public Object decode(Response response, Type type) throws IOException {
+  @Override
+  public Object decode(Response response, Type type) throws IOException {
     if (response.body() == null) {
       return null;
     }
diff --git a/jackson/src/main/java/feign/jackson/JacksonEncoder.java b/jackson/src/main/java/feign/jackson/JacksonEncoder.java
index 1b8db303fb..59ff1128d6 100644
--- a/jackson/src/main/java/feign/jackson/JacksonEncoder.java
+++ b/jackson/src/main/java/feign/jackson/JacksonEncoder.java
@@ -21,13 +21,16 @@
 import com.fasterxml.jackson.databind.Module;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.SerializationFeature;
+
+import java.lang.reflect.Type;
+import java.util.Collections;
+
 import feign.RequestTemplate;
 import feign.codec.EncodeException;
 import feign.codec.Encoder;
-import java.lang.reflect.Type;
-import java.util.Collections;
 
 public class JacksonEncoder implements Encoder {
+
   private final ObjectMapper mapper;
 
   public JacksonEncoder() {
@@ -36,16 +39,17 @@ public JacksonEncoder() {
 
   public JacksonEncoder(Iterable modules) {
     this(new ObjectMapper()
-        .setSerializationInclusion(JsonInclude.Include.NON_NULL)
-        .configure(SerializationFeature.INDENT_OUTPUT, true)
-        .registerModules(modules));
+             .setSerializationInclusion(JsonInclude.Include.NON_NULL)
+             .configure(SerializationFeature.INDENT_OUTPUT, true)
+             .registerModules(modules));
   }
 
   public JacksonEncoder(ObjectMapper mapper) {
     this.mapper = mapper;
   }
 
-  @Override public void encode(Object object, Type bodyType, RequestTemplate template) {
+  @Override
+  public void encode(Object object, Type bodyType, RequestTemplate template) {
     try {
       JavaType javaType = mapper.getTypeFactory().constructType(bodyType);
       template.body(mapper.writerWithType(javaType).writeValueAsString(object));
diff --git a/jackson/src/test/java/feign/jackson/JacksonCodecTest.java b/jackson/src/test/java/feign/jackson/JacksonCodecTest.java
index 3bcaaf06f8..044045b7d5 100644
--- a/jackson/src/test/java/feign/jackson/JacksonCodecTest.java
+++ b/jackson/src/test/java/feign/jackson/JacksonCodecTest.java
@@ -10,8 +10,9 @@
 import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
 import com.fasterxml.jackson.databind.module.SimpleModule;
 import com.fasterxml.jackson.databind.ser.std.StdSerializer;
-import feign.RequestTemplate;
-import feign.Response;
+
+import org.junit.Test;
+
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.Collection;
@@ -20,7 +21,9 @@
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
-import org.junit.Test;
+
+import feign.RequestTemplate;
+import feign.Response;
 
 import static feign.Util.UTF_8;
 import static feign.assertj.FeignAssertions.assertThat;
@@ -29,7 +32,19 @@
 
 public class JacksonCodecTest {
 
-  @Test public void encodesMapObjectNumericalValuesAsInteger() throws Exception {
+  private String zonesJson = ""//
+                             + "[\n"//
+                             + "  {\n"//
+                             + "    \"name\": \"denominator.io.\"\n"//
+                             + "  },\n"//
+                             + "  {\n"//
+                             + "    \"name\": \"denominator.io.\",\n"//
+                             + "    \"id\": \"ABCD\"\n"//
+                             + "  }\n"//
+                             + "]\n";
+
+  @Test
+  public void encodesMapObjectNumericalValuesAsInteger() throws Exception {
     Map map = new LinkedHashMap();
     map.put("foo", 1);
 
@@ -37,12 +52,13 @@ public class JacksonCodecTest {
     new JacksonEncoder().encode(map, map.getClass(), template);
 
     assertThat(template).hasBody(""//
-        + "{\n" //
-        + "  \"foo\" : 1\n" //
-        + "}");
+                                 + "{\n" //
+                                 + "  \"foo\" : 1\n" //
+                                 + "}");
   }
 
-  @Test public void encodesFormParams() throws Exception {
+  @Test
+  public void encodesFormParams() throws Exception {
     Map form = new LinkedHashMap();
     form.put("foo", 1);
     form.put("bar", Arrays.asList(2, 3));
@@ -52,13 +68,77 @@ public class JacksonCodecTest {
     }.getType(), template);
 
     assertThat(template).hasBody(""//
-        + "{\n" //
-        + "  \"foo\" : 1,\n" //
-        + "  \"bar\" : [ 2, 3 ]\n" //
-        + "}");
+                                 + "{\n" //
+                                 + "  \"foo\" : 1,\n" //
+                                 + "  \"bar\" : [ 2, 3 ]\n" //
+                                 + "}");
+  }
+
+  @Test
+  public void decodes() throws Exception {
+    List zones = new LinkedList();
+    zones.add(new Zone("denominator.io."));
+    zones.add(new Zone("denominator.io.", "ABCD"));
+
+    Response response =
+        Response.create(200, "OK", Collections.>emptyMap(), zonesJson,
+                        UTF_8);
+    assertEquals(zones, new JacksonDecoder().decode(response, new TypeReference>() {
+    }.getType()));
+  }
+
+  @Test
+  public void nullBodyDecodesToNull() throws Exception {
+    Response
+        response =
+        Response
+            .create(204, "OK", Collections.>emptyMap(), (byte[]) null);
+    assertNull(new JacksonDecoder().decode(response, String.class));
+  }
+
+  @Test
+  public void customDecoder() throws Exception {
+    JacksonDecoder decoder = new JacksonDecoder(
+        Arrays.asList(
+            new SimpleModule().addDeserializer(Zone.class, new ZoneDeserializer())));
+
+    List zones = new LinkedList();
+    zones.add(new Zone("DENOMINATOR.IO."));
+    zones.add(new Zone("DENOMINATOR.IO.", "ABCD"));
+
+    Response response =
+        Response.create(200, "OK", Collections.>emptyMap(), zonesJson,
+                        UTF_8);
+    assertEquals(zones, decoder.decode(response, new TypeReference>() {
+    }.getType()));
+  }
+
+  @Test
+  public void customEncoder() throws Exception {
+    JacksonEncoder encoder = new JacksonEncoder(
+        Arrays.asList(new SimpleModule().addSerializer(Zone.class, new ZoneSerializer())));
+
+    List zones = new LinkedList();
+    zones.add(new Zone("denominator.io."));
+    zones.add(new Zone("denominator.io.", "abcd"));
+
+    RequestTemplate template = new RequestTemplate();
+    encoder.encode(zones, new TypeReference>() {
+    }.getType(), template);
+
+    assertThat(template).hasBody("" //
+                                 + "[ {\n"
+                                 + "  \"name\" : \"DENOMINATOR.IO.\"\n"
+                                 + "}, {\n"
+                                 + "  \"name\" : \"DENOMINATOR.IO.\",\n"
+                                 + "  \"id\" : \"ABCD\"\n"
+                                 + "} ]");
   }
 
   static class Zone extends LinkedHashMap {
+
+    private static final long serialVersionUID = 1L;
+
     Zone() {
       // for reflective instantiation.
     }
@@ -73,38 +153,10 @@ static class Zone extends LinkedHashMap {
         put("id", id);
       }
     }
-
-    private static final long serialVersionUID = 1L;
-  }
-
-  @Test public void decodes() throws Exception {
-    List zones = new LinkedList();
-    zones.add(new Zone("denominator.io."));
-    zones.add(new Zone("denominator.io.", "ABCD"));
-
-    Response response =
-        Response.create(200, "OK", Collections.>emptyMap(), zonesJson, UTF_8);
-    assertEquals(zones, new JacksonDecoder().decode(response, new TypeReference>() {
-    }.getType()));
   }
 
-  @Test public void nullBodyDecodesToNull() throws Exception {
-    Response response = Response.create(204, "OK", Collections.>emptyMap(), (byte[]) null);
-    assertNull(new JacksonDecoder().decode(response, String.class));
-  }
-
-  private String zonesJson = ""//
-      + "[\n"//
-      + "  {\n"//
-      + "    \"name\": \"denominator.io.\"\n"//
-      + "  },\n"//
-      + "  {\n"//
-      + "    \"name\": \"denominator.io.\",\n"//
-      + "    \"id\": \"ABCD\"\n"//
-      + "  }\n"//
-      + "]\n";
-
   static class ZoneDeserializer extends StdDeserializer {
+
     public ZoneDeserializer() {
       super(Zone.class);
     }
@@ -124,52 +176,21 @@ public Zone deserialize(JsonParser jp, DeserializationContext ctxt) throws IOExc
     }
   }
 
-  @Test public void customDecoder() throws Exception {
-    JacksonDecoder decoder = new JacksonDecoder(
-        Arrays.asList(new SimpleModule().addDeserializer(Zone.class, new ZoneDeserializer())));
-
-    List zones = new LinkedList();
-    zones.add(new Zone("DENOMINATOR.IO."));
-    zones.add(new Zone("DENOMINATOR.IO.", "ABCD"));
-
-    Response response =
-        Response.create(200, "OK", Collections.>emptyMap(), zonesJson, UTF_8);
-    assertEquals(zones, decoder.decode(response, new TypeReference>(){}.getType()));
-  }
-
   static class ZoneSerializer extends StdSerializer {
+
     public ZoneSerializer() {
       super(Zone.class);
     }
 
-    @Override public void serialize(Zone value, JsonGenerator jgen, SerializerProvider provider)
+    @Override
+    public void serialize(Zone value, JsonGenerator jgen, SerializerProvider provider)
         throws IOException {
       jgen.writeStartObject();
-      for(Map.Entry entry : value.entrySet()) {
+      for (Map.Entry entry : value.entrySet()) {
         jgen.writeFieldName(entry.getKey());
         jgen.writeString(entry.getValue().toString().toUpperCase());
       }
       jgen.writeEndObject();
     }
   }
-
-  @Test public void customEncoder() throws Exception {
-    JacksonEncoder encoder = new JacksonEncoder(
-        Arrays.asList(new SimpleModule().addSerializer(Zone.class, new ZoneSerializer())));
-
-    List zones = new LinkedList();
-    zones.add(new Zone("denominator.io."));
-    zones.add(new Zone("denominator.io.", "abcd"));
-
-    RequestTemplate template = new RequestTemplate();
-    encoder.encode(zones, new TypeReference>(){}.getType(), template);
-
-    assertThat(template).hasBody("" //
-        + "[ {\n"
-        + "  \"name\" : \"DENOMINATOR.IO.\"\n"
-        + "}, {\n"
-        + "  \"name\" : \"DENOMINATOR.IO.\",\n"
-        + "  \"id\" : \"ABCD\"\n"
-        + "} ]");
-  }
 }
diff --git a/jackson/src/test/java/feign/jackson/examples/GitHubExample.java b/jackson/src/test/java/feign/jackson/examples/GitHubExample.java
index 5ec2c2e975..992637ec65 100644
--- a/jackson/src/test/java/feign/jackson/examples/GitHubExample.java
+++ b/jackson/src/test/java/feign/jackson/examples/GitHubExample.java
@@ -1,42 +1,46 @@
 package feign.jackson.examples;
 
+import java.util.List;
+
 import feign.Feign;
 import feign.Param;
 import feign.RequestLine;
 import feign.jackson.JacksonDecoder;
-import java.util.List;
 
 /**
  * adapted from {@code com.example.retrofit.GitHubClient}
  */
 public class GitHubExample {
+
+  public static void main(String... args) {
+    GitHub github = Feign.builder()
+        .decoder(new JacksonDecoder())
+        .target(GitHub.class, "https://api.github.com");
+
+    System.out.println("Let's fetch and print a list of the contributors to this library.");
+    List contributors = github.contributors("netflix", "feign");
+    for (Contributor contributor : contributors) {
+      System.out.println(contributor.login + " (" + contributor.contributions + ")");
+    }
+  }
+
   interface GitHub {
+
     @RequestLine("GET /repos/{owner}/{repo}/contributors")
     List contributors(@Param("owner") String owner, @Param("repo") String repo);
   }
 
   static class Contributor {
+
     private String login;
     private int contributions;
 
     void setLogin(String login) {
-        this.login = login;
+      this.login = login;
     }
 
     void setContributions(int contributions) {
-        this.contributions = contributions;
-    }
-  }
-
-  public static void main(String... args) {
-    GitHub github = Feign.builder()
-                         .decoder(new JacksonDecoder())
-                         .target(GitHub.class, "https://api.github.com");
-
-    System.out.println("Let's fetch and print a list of the contributors to this library.");
-    List contributors = github.contributors("netflix", "feign");
-    for (Contributor contributor : contributors) {
-      System.out.println(contributor.login + " (" + contributor.contributions + ")");
+      this.contributions = contributions;
     }
   }
 }
diff --git a/jaxb/src/main/java/feign/jaxb/JAXBContextFactory.java b/jaxb/src/main/java/feign/jaxb/JAXBContextFactory.java
index b12ca5551e..c3d191656f 100644
--- a/jaxb/src/main/java/feign/jaxb/JAXBContextFactory.java
+++ b/jaxb/src/main/java/feign/jaxb/JAXBContextFactory.java
@@ -15,21 +15,26 @@
  */
 package feign.jaxb;
 
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
 import javax.xml.bind.JAXBContext;
 import javax.xml.bind.JAXBException;
 import javax.xml.bind.Marshaller;
 import javax.xml.bind.PropertyException;
 import javax.xml.bind.Unmarshaller;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
 
 /**
- * Creates and caches JAXB contexts as well as creates Marshallers and Unmarshallers for each context.
+ * Creates and caches JAXB contexts as well as creates Marshallers and Unmarshallers for each
+ * context.
  */
 public final class JAXBContextFactory {
-  private final ConcurrentHashMap jaxbContexts = new ConcurrentHashMap(64);
+
+  private final ConcurrentHashMap
+      jaxbContexts =
+      new ConcurrentHashMap(64);
   private final Map properties;
 
   private JAXBContextFactory(Map properties) {
@@ -76,6 +81,7 @@ private JAXBContext getContext(Class clazz) throws JAXBException {
    * Creates instances of {@link feign.jaxb.JAXBContextFactory}
    */
   public static class Builder {
+
     private final Map properties = new HashMap(5);
 
     /**
diff --git a/jaxb/src/main/java/feign/jaxb/JAXBDecoder.java b/jaxb/src/main/java/feign/jaxb/JAXBDecoder.java
index 51775f9f6c..3e593ff695 100644
--- a/jaxb/src/main/java/feign/jaxb/JAXBDecoder.java
+++ b/jaxb/src/main/java/feign/jaxb/JAXBDecoder.java
@@ -15,20 +15,18 @@
  */
 package feign.jaxb;
 
-import feign.Response;
-import feign.codec.DecodeException;
-import feign.codec.Decoder;
 import java.io.IOException;
 import java.lang.reflect.Type;
+
 import javax.xml.bind.JAXBException;
 import javax.xml.bind.Unmarshaller;
 
+import feign.Response;
+import feign.codec.DecodeException;
+import feign.codec.Decoder;
+
 /**
- * Decodes responses using JAXB.
- * 
- *

- * Basic example with with Feign.Builder: - *

+ * Decodes responses using JAXB.

Basic example with with Feign.Builder:

*
  * JAXBContextFactory jaxbFactory = new JAXBContextFactory.Builder()
  *      .withMarshallerJAXBEncoding("UTF-8")
@@ -39,20 +37,22 @@
  *            .decoder(new JAXBDecoder(jaxbFactory))
  *            .target(MyApi.class, "http://api");
  * 
- *

- * The JAXBContextFactory should be reused across requests as it caches the created JAXB contexts. - *

+ *

The JAXBContextFactory should be reused across requests as it caches the created JAXB + * contexts.

*/ public class JAXBDecoder implements Decoder { + private final JAXBContextFactory jaxbContextFactory; public JAXBDecoder(JAXBContextFactory jaxbContextFactory) { this.jaxbContextFactory = jaxbContextFactory; } - @Override public Object decode(Response response, Type type) throws IOException { + @Override + public Object decode(Response response, Type type) throws IOException { if (!(type instanceof Class)) { - throw new UnsupportedOperationException("JAXB only supports decoding raw types. Found " + type); + throw new UnsupportedOperationException( + "JAXB only supports decoding raw types. Found " + type); } try { Unmarshaller unmarshaller = jaxbContextFactory.createUnmarshaller((Class) type); diff --git a/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java b/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java index 79c546ef89..9ed39ae380 100644 --- a/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java +++ b/jaxb/src/main/java/feign/jaxb/JAXBEncoder.java @@ -15,21 +15,18 @@ */ package feign.jaxb; -import feign.RequestTemplate; -import feign.codec.EncodeException; -import feign.codec.Encoder; import java.io.StringWriter; -import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; + import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; +import feign.RequestTemplate; +import feign.codec.EncodeException; +import feign.codec.Encoder; + /** - * Encodes requests using JAXB. - *
- *

- * Basic example with with Feign.Builder: - *

+ * Encodes requests using JAXB.

Basic example with with Feign.Builder:

*
  * JAXBContextFactory jaxbFactory = new JAXBContextFactory.Builder()
  *      .withMarshallerJAXBEncoding("UTF-8")
@@ -40,20 +37,22 @@
  *            .encoder(new JAXBEncoder(jaxbFactory))
  *            .target(MyApi.class, "http://api");
  * 
- *

- * The JAXBContextFactory should be reused across requests as it caches the created JAXB contexts. - *

+ *

The JAXBContextFactory should be reused across requests as it caches the created JAXB + * contexts.

*/ public class JAXBEncoder implements Encoder { + private final JAXBContextFactory jaxbContextFactory; public JAXBEncoder(JAXBContextFactory jaxbContextFactory) { this.jaxbContextFactory = jaxbContextFactory; } - @Override public void encode(Object object, Type bodyType, RequestTemplate template) { + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) { if (!(bodyType instanceof Class)) { - throw new UnsupportedOperationException("JAXB only supports encoding raw types. Found " + bodyType); + throw new UnsupportedOperationException( + "JAXB only supports encoding raw types. Found " + bodyType); } try { Marshaller marshaller = jaxbContextFactory.createMarshaller((Class) bodyType); diff --git a/jaxb/src/test/java/feign/jaxb/JAXBCodecTest.java b/jaxb/src/test/java/feign/jaxb/JAXBCodecTest.java index 051e644faf..bf8f395492 100644 --- a/jaxb/src/test/java/feign/jaxb/JAXBCodecTest.java +++ b/jaxb/src/test/java/feign/jaxb/JAXBCodecTest.java @@ -15,70 +15,65 @@ */ package feign.jaxb; -import feign.RequestTemplate; -import feign.Response; -import feign.codec.Encoder; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + import java.lang.reflect.Type; import java.util.Collection; import java.util.Collections; import java.util.Map; + import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; + +import feign.RequestTemplate; +import feign.Response; +import feign.codec.Encoder; import static feign.Util.UTF_8; import static feign.assertj.FeignAssertions.assertThat; import static org.junit.Assert.assertEquals; public class JAXBCodecTest { - @Rule public final ExpectedException thrown = ExpectedException.none(); - - @XmlRootElement @XmlAccessorType(XmlAccessType.FIELD) static class MockObject { - - @XmlElement private String value; - - @Override public boolean equals(Object obj) { - if (obj instanceof MockObject) { - MockObject other = (MockObject) obj; - return value.equals(other.value); - } - return false; - } - @Override public int hashCode() { - return value != null ? value.hashCode() : 0; - } - } + @Rule + public final ExpectedException thrown = ExpectedException.none(); - @Test public void encodesXml() throws Exception { + @Test + public void encodesXml() throws Exception { MockObject mock = new MockObject(); mock.value = "Test"; RequestTemplate template = new RequestTemplate(); - new JAXBEncoder(new JAXBContextFactory.Builder().build()).encode(mock, MockObject.class, template); + new JAXBEncoder(new JAXBContextFactory.Builder().build()) + .encode(mock, MockObject.class, template); assertThat(template).hasBody( "Test"); } - @Test public void doesntEncodeParameterizedTypes() throws Exception { + @Test + public void doesntEncodeParameterizedTypes() throws Exception { thrown.expect(UnsupportedOperationException.class); - thrown.expectMessage("JAXB only supports encoding raw types. Found java.util.Map"); + thrown.expectMessage( + "JAXB only supports encoding raw types. Found java.util.Map"); class ParameterizedHolder { + Map field; } Type parameterized = ParameterizedHolder.class.getDeclaredField("field").getGenericType(); RequestTemplate template = new RequestTemplate(); - new JAXBEncoder(new JAXBContextFactory.Builder().build()).encode(Collections.emptyMap(), parameterized, template); + new JAXBEncoder(new JAXBContextFactory.Builder().build()) + .encode(Collections.emptyMap(), parameterized, template); } - @Test public void encodesXmlWithCustomJAXBEncoding() throws Exception { + @Test + public void encodesXmlWithCustomJAXBEncoding() throws Exception { JAXBContextFactory jaxbContextFactory = new JAXBContextFactory.Builder().withMarshallerJAXBEncoding("UTF-16").build(); @@ -91,12 +86,14 @@ class ParameterizedHolder { encoder.encode(mock, MockObject.class, template); assertThat(template).hasBody("Test"); + + "standalone=\"yes\"?>Test"); } - @Test public void encodesXmlWithCustomJAXBSchemaLocation() throws Exception { + @Test + public void encodesXmlWithCustomJAXBSchemaLocation() throws Exception { JAXBContextFactory jaxbContextFactory = - new JAXBContextFactory.Builder().withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd") + new JAXBContextFactory.Builder() + .withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd") .build(); Encoder encoder = new JAXBEncoder(jaxbContextFactory); @@ -108,14 +105,18 @@ class ParameterizedHolder { encoder.encode(mock, MockObject.class, template); assertThat(template).hasBody("" + - "Test"); + "standalone=\"yes\"?>" + + + "Test"); } - @Test public void encodesXmlWithCustomJAXBNoNamespaceSchemaLocation() throws Exception { + @Test + public void encodesXmlWithCustomJAXBNoNamespaceSchemaLocation() throws Exception { JAXBContextFactory jaxbContextFactory = - new JAXBContextFactory.Builder().withMarshallerNoNamespaceSchemaLocation("http://apihost/schema.xsd").build(); + new JAXBContextFactory.Builder() + .withMarshallerNoNamespaceSchemaLocation("http://apihost/schema.xsd").build(); Encoder encoder = new JAXBEncoder(jaxbContextFactory); @@ -126,12 +127,14 @@ class ParameterizedHolder { encoder.encode(mock, MockObject.class, template); assertThat(template).hasBody("" + - "Test"); + "standalone=\"yes\"?>" + + "Test"); } - @Test public void encodesXmlWithCustomJAXBFormattedOutput() { + @Test + public void encodesXmlWithCustomJAXBFormattedOutput() { JAXBContextFactory jaxbContextFactory = new JAXBContextFactory.Builder().withMarshallerFormattedOutput(true).build(); @@ -157,31 +160,63 @@ class ParameterizedHolder { .toString()); } - @Test public void decodesXml() throws Exception { + @Test + public void decodesXml() throws Exception { MockObject mock = new MockObject(); mock.value = "Test"; String mockXml = "" - + "Test"; + + "Test"; - Response response = Response.create(200, "OK", Collections.>emptyMap(), mockXml, UTF_8); + Response + response = + Response + .create(200, "OK", Collections.>emptyMap(), mockXml, UTF_8); JAXBDecoder decoder = new JAXBDecoder(new JAXBContextFactory.Builder().build()); assertEquals(mock, decoder.decode(response, MockObject.class)); } - @Test public void doesntDecodeParameterizedTypes() throws Exception { + @Test + public void doesntDecodeParameterizedTypes() throws Exception { thrown.expect(UnsupportedOperationException.class); - thrown.expectMessage("JAXB only supports decoding raw types. Found java.util.Map"); + thrown.expectMessage( + "JAXB only supports decoding raw types. Found java.util.Map"); class ParameterizedHolder { + Map field; } Type parameterized = ParameterizedHolder.class.getDeclaredField("field").getGenericType(); - Response response = Response.create(200, "OK", Collections.>emptyMap(), "", UTF_8); + Response + response = + Response + .create(200, "OK", Collections.>emptyMap(), "", UTF_8); new JAXBDecoder(new JAXBContextFactory.Builder().build()).decode(response, parameterized); } + + @XmlRootElement + @XmlAccessorType(XmlAccessType.FIELD) + static class MockObject { + + @XmlElement + private String value; + + @Override + public boolean equals(Object obj) { + if (obj instanceof MockObject) { + MockObject other = (MockObject) obj; + return value.equals(other.value); + } + return false; + } + + @Override + public int hashCode() { + return value != null ? value.hashCode() : 0; + } + } } diff --git a/jaxb/src/test/java/feign/jaxb/JAXBContextFactoryTest.java b/jaxb/src/test/java/feign/jaxb/JAXBContextFactoryTest.java index 4410a666b2..daf4fa71b1 100644 --- a/jaxb/src/test/java/feign/jaxb/JAXBContextFactoryTest.java +++ b/jaxb/src/test/java/feign/jaxb/JAXBContextFactoryTest.java @@ -15,46 +15,63 @@ */ package feign.jaxb; -import javax.xml.bind.Marshaller; import org.junit.Test; +import javax.xml.bind.Marshaller; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; public class JAXBContextFactoryTest { - @Test public void buildsMarshallerWithJAXBEncodingProperty() throws Exception { - JAXBContextFactory factory = new JAXBContextFactory.Builder().withMarshallerJAXBEncoding("UTF-16").build(); + + @Test + public void buildsMarshallerWithJAXBEncodingProperty() throws Exception { + JAXBContextFactory + factory = + new JAXBContextFactory.Builder().withMarshallerJAXBEncoding("UTF-16").build(); Marshaller marshaller = factory.createMarshaller(Object.class); assertEquals("UTF-16", marshaller.getProperty(Marshaller.JAXB_ENCODING)); } - @Test public void buildsMarshallerWithSchemaLocationProperty() throws Exception { + @Test + public void buildsMarshallerWithSchemaLocationProperty() throws Exception { JAXBContextFactory factory = - new JAXBContextFactory.Builder().withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd") + new JAXBContextFactory.Builder() + .withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd") .build(); Marshaller marshaller = factory.createMarshaller(Object.class); - assertEquals("http://apihost http://apihost/schema.xsd", marshaller.getProperty(Marshaller.JAXB_SCHEMA_LOCATION)); + assertEquals("http://apihost http://apihost/schema.xsd", + marshaller.getProperty(Marshaller.JAXB_SCHEMA_LOCATION)); } - @Test public void buildsMarshallerWithNoNamespaceSchemaLocationProperty() throws Exception { + @Test + public void buildsMarshallerWithNoNamespaceSchemaLocationProperty() throws Exception { JAXBContextFactory factory = - new JAXBContextFactory.Builder().withMarshallerNoNamespaceSchemaLocation("http://apihost/schema.xsd").build(); + new JAXBContextFactory.Builder() + .withMarshallerNoNamespaceSchemaLocation("http://apihost/schema.xsd").build(); Marshaller marshaller = factory.createMarshaller(Object.class); - assertEquals("http://apihost/schema.xsd", marshaller.getProperty(Marshaller.JAXB_NO_NAMESPACE_SCHEMA_LOCATION)); + assertEquals("http://apihost/schema.xsd", + marshaller.getProperty(Marshaller.JAXB_NO_NAMESPACE_SCHEMA_LOCATION)); } - @Test public void buildsMarshallerWithFormattedOutputProperty() throws Exception { - JAXBContextFactory factory = new JAXBContextFactory.Builder().withMarshallerFormattedOutput(true).build(); + @Test + public void buildsMarshallerWithFormattedOutputProperty() throws Exception { + JAXBContextFactory + factory = + new JAXBContextFactory.Builder().withMarshallerFormattedOutput(true).build(); Marshaller marshaller = factory.createMarshaller(Object.class); assertTrue((Boolean) marshaller.getProperty(Marshaller.JAXB_FORMATTED_OUTPUT)); } - @Test public void buildsMarshallerWithFragmentProperty() throws Exception { - JAXBContextFactory factory = new JAXBContextFactory.Builder().withMarshallerFragment(true).build(); + @Test + public void buildsMarshallerWithFragmentProperty() throws Exception { + JAXBContextFactory + factory = + new JAXBContextFactory.Builder().withMarshallerFragment(true).build(); Marshaller marshaller = factory.createMarshaller(Object.class); assertTrue((Boolean) marshaller.getProperty(Marshaller.JAXB_FRAGMENT)); diff --git a/jaxb/src/test/java/feign/jaxb/examples/AWSSignatureVersion4.java b/jaxb/src/test/java/feign/jaxb/examples/AWSSignatureVersion4.java index 683638fdc2..fbeb22a8aa 100644 --- a/jaxb/src/test/java/feign/jaxb/examples/AWSSignatureVersion4.java +++ b/jaxb/src/test/java/feign/jaxb/examples/AWSSignatureVersion4.java @@ -15,21 +15,30 @@ */ package feign.jaxb.examples; -import feign.Request; -import feign.RequestTemplate; import java.net.URI; import java.security.MessageDigest; import java.text.SimpleDateFormat; import java.util.Date; import java.util.TimeZone; + import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; +import feign.Request; +import feign.RequestTemplate; + import static feign.Util.UTF_8; // http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html public class AWSSignatureVersion4 { + private static final String + EMPTY_STRING_HASH = + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + private static final SimpleDateFormat iso8601 = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"); + static { + iso8601.setTimeZone(TimeZone.getTimeZone("GMT")); + } String region = "us-east-1"; String service = "iam"; String accessKey; @@ -40,45 +49,6 @@ public AWSSignatureVersion4(String accessKey, String secretKey) { this.secretKey = secretKey; } - public Request apply(RequestTemplate input) { - if (!input.headers().isEmpty()) throw new UnsupportedOperationException("headers not supported"); - if (input.body() != null) throw new UnsupportedOperationException("body not supported"); - - String host = URI.create(input.url()).getHost(); - - String timestamp; - synchronized (iso8601) { - timestamp = iso8601.format(new Date()); - } - - String credentialScope = String.format("%s/%s/%s/%s", timestamp.substring(0, 8), region, service, "aws4_request"); - - input.query("X-Amz-Algorithm", "AWS4-HMAC-SHA256"); - input.query("X-Amz-Credential", accessKey + "/" + credentialScope); - input.query("X-Amz-Date", timestamp); - input.query("X-Amz-SignedHeaders", "host"); - input.header("Host", host); - - String canonicalString = canonicalString(input, host); - String toSign = toSign(timestamp, credentialScope, canonicalString); - - byte[] signatureKey = signatureKey(secretKey, timestamp); - String signature = hex(hmacSHA256(toSign, signatureKey)); - - input.query("X-Amz-Signature", signature); - - return input.request(); - } - - byte[] signatureKey(String secretKey, String timestamp) { - byte[] kSecret = ("AWS4" + secretKey).getBytes(UTF_8); - byte[] kDate = hmacSHA256(timestamp.substring(0, 8), kSecret); - byte[] kRegion = hmacSHA256(region, kDate); - byte[] kService = hmacSHA256(service, kRegion); - byte[] kSigning = hmacSHA256("aws4_request", kService); - return kSigning; - } - static byte[] hmacSHA256(String data, byte[] key) { try { String algorithm = "HmacSHA256"; @@ -90,8 +60,6 @@ static byte[] hmacSHA256(String data, byte[] key) { } } - private static final String EMPTY_STRING_HASH = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; - private static String canonicalString(RequestTemplate input, String host) { StringBuilder canonicalRequest = new StringBuilder(); // HTTPRequestMethod + '\n' + @@ -114,7 +82,8 @@ private static String canonicalString(RequestTemplate input, String host) { // HexEncode(Hash(Payload)) String bodyText = - input.charset() != null && input.body() != null ? new String(input.body(), input.charset()) : null; + input.charset() != null && input.body() != null ? new String(input.body(), input.charset()) + : null; if (bodyText != null) { canonicalRequest.append(hex(sha256(bodyText))); } else { @@ -153,9 +122,48 @@ static byte[] sha256(String data) { } } - private static final SimpleDateFormat iso8601 = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"); + public Request apply(RequestTemplate input) { + if (!input.headers().isEmpty()) { + throw new UnsupportedOperationException("headers not supported"); + } + if (input.body() != null) { + throw new UnsupportedOperationException("body not supported"); + } - static { - iso8601.setTimeZone(TimeZone.getTimeZone("GMT")); + String host = URI.create(input.url()).getHost(); + + String timestamp; + synchronized (iso8601) { + timestamp = iso8601.format(new Date()); + } + + String + credentialScope = + String.format("%s/%s/%s/%s", timestamp.substring(0, 8), region, service, "aws4_request"); + + input.query("X-Amz-Algorithm", "AWS4-HMAC-SHA256"); + input.query("X-Amz-Credential", accessKey + "/" + credentialScope); + input.query("X-Amz-Date", timestamp); + input.query("X-Amz-SignedHeaders", "host"); + input.header("Host", host); + + String canonicalString = canonicalString(input, host); + String toSign = toSign(timestamp, credentialScope, canonicalString); + + byte[] signatureKey = signatureKey(secretKey, timestamp); + String signature = hex(hmacSHA256(toSign, signatureKey)); + + input.query("X-Amz-Signature", signature); + + return input.request(); + } + + byte[] signatureKey(String secretKey, String timestamp) { + byte[] kSecret = ("AWS4" + secretKey).getBytes(UTF_8); + byte[] kDate = hmacSHA256(timestamp.substring(0, 8), kSecret); + byte[] kRegion = hmacSHA256(region, kDate); + byte[] kService = hmacSHA256(service, kRegion); + byte[] kSigning = hmacSHA256("aws4_request", kService); + return kSigning; } } diff --git a/jaxb/src/test/java/feign/jaxb/examples/IAMExample.java b/jaxb/src/test/java/feign/jaxb/examples/IAMExample.java index e8443ffdf1..8318ce1e67 100644 --- a/jaxb/src/test/java/feign/jaxb/examples/IAMExample.java +++ b/jaxb/src/test/java/feign/jaxb/examples/IAMExample.java @@ -15,6 +15,12 @@ */ package feign.jaxb.examples; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlType; + import feign.Feign; import feign.Request; import feign.RequestLine; @@ -22,18 +28,9 @@ import feign.Target; import feign.jaxb.JAXBContextFactory; import feign.jaxb.JAXBDecoder; -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; -import javax.xml.bind.annotation.XmlType; public class IAMExample { - interface IAM { - @RequestLine("GET /?Action=GetUser&Version=2010-05-08") GetUserResponse userResponse(); - } - public static void main(String... args) { IAM iam = Feign.builder() .decoder(new JAXBDecoder(new JAXBContextFactory.Builder().build())) @@ -43,40 +40,61 @@ public static void main(String... args) { System.out.println("UserId: " + response.result.user.id); } + interface IAM { + + @RequestLine("GET /?Action=GetUser&Version=2010-05-08") + GetUserResponse userResponse(); + } + static class IAMTarget extends AWSSignatureVersion4 implements Target { - @Override public Class type() { + private IAMTarget(String accessKey, String secretKey) { + super(accessKey, secretKey); + } + + @Override + public Class type() { return IAM.class; } - @Override public String name() { + @Override + public String name() { return "iam"; } - @Override public String url() { + @Override + public String url() { return "https://iam.amazonaws.com"; } - private IAMTarget(String accessKey, String secretKey) { - super(accessKey, secretKey); - } - - @Override public Request apply(RequestTemplate in) { + @Override + public Request apply(RequestTemplate in) { in.insert(0, url()); return super.apply(in); } } @XmlRootElement(name = "GetUserResponse", namespace = "https://iam.amazonaws.com/doc/2010-05-08/") - @XmlAccessorType(XmlAccessType.FIELD) static class GetUserResponse { - @XmlElement(name = "GetUserResult") private GetUserResult result; + @XmlAccessorType(XmlAccessType.FIELD) + static class GetUserResponse { + + @XmlElement(name = "GetUserResult") + private GetUserResult result; } - @XmlAccessorType(XmlAccessType.FIELD) @XmlType(name = "GetUserResult") static class GetUserResult { - @XmlElement(name = "User") private User user; + @XmlAccessorType(XmlAccessType.FIELD) + @XmlType(name = "GetUserResult") + static class GetUserResult { + + @XmlElement(name = "User") + private User user; } - @XmlAccessorType(XmlAccessType.FIELD) @XmlType(name = "User") static class User { - @XmlElement(name = "UserId") private String id; + @XmlAccessorType(XmlAccessType.FIELD) + @XmlType(name = "User") + static class User { + + @XmlElement(name = "UserId") + private String id; } } diff --git a/jaxrs/src/main/java/feign/jaxrs/JAXRSContract.java b/jaxrs/src/main/java/feign/jaxrs/JAXRSContract.java index 34d9526f30..50c11d1ba0 100644 --- a/jaxrs/src/main/java/feign/jaxrs/JAXRSContract.java +++ b/jaxrs/src/main/java/feign/jaxrs/JAXRSContract.java @@ -15,8 +15,9 @@ */ package feign.jaxrs; -import feign.Contract; -import feign.MethodMetadata; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Collection; import javax.ws.rs.Consumes; import javax.ws.rs.FormParam; @@ -26,18 +27,19 @@ import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; -import java.util.Collection; + +import feign.Contract; +import feign.MethodMetadata; import static feign.Util.checkState; import static feign.Util.emptyToNull; /** - * Please refer to the - * Feign JAX-RS README. + * Please refer to the Feign + * JAX-RS README. */ public final class JAXRSContract extends Contract.BaseContract { + static final String ACCEPT = "Accept"; static final String CONTENT_TYPE = "Content-Type"; @@ -47,9 +49,10 @@ public MethodMetadata parseAndValidatateMetadata(Method 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()); + checkState(pathValue != null, "Path.value() was empty on type %s", + method.getDeclaringClass().getName()); if (!pathValue.startsWith("/")) { - pathValue = "/" + pathValue; + pathValue = "/" + pathValue; } md.template().insert(0, pathValue); } @@ -57,13 +60,15 @@ public MethodMetadata parseAndValidatateMetadata(Method method) { } @Override - protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) { + protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, + Method method) { Class annotationType = methodAnnotation.annotationType(); HttpMethod http = annotationType.getAnnotation(HttpMethod.class); if (http != null) { checkState(data.template().method() == null, - "Method %s contains multiple HTTP methods. Found: %s and %s", method.getName(), data.template() - .method(), http.value()); + "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 == Path.class) { String pathValue = emptyToNull(Path.class.cast(methodAnnotation).value()); @@ -75,44 +80,51 @@ protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodA data.template().append(methodAnnotationValue); } else if (annotationType == Produces.class) { 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()); + 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) { 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()); + 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); } } @Override - protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) { + protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, + int paramIndex) { boolean isHttpParam = false; for (Annotation parameterAnnotation : annotations) { Class 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); + 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); + checkState(emptyToNull(name) != null, "QueryParam.value() was empty on parameter %s", + paramIndex); Collection 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); + checkState(emptyToNull(name) != null, "HeaderParam.value() was empty on parameter %s", + paramIndex); Collection 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); + checkState(emptyToNull(name) != null, "FormParam.value() was empty on parameter %s", + paramIndex); data.formParams().add(name); nameParam(data, name, paramIndex); isHttpParam = true; diff --git a/jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java b/jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java index a4b286789f..6d74a9af33 100644 --- a/jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java +++ b/jaxrs/src/test/java/feign/jaxrs/JAXRSContractTest.java @@ -15,14 +15,17 @@ */ package feign.jaxrs; -import feign.MethodMetadata; -import feign.Response; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.net.URI; import java.util.List; + import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.FormParam; @@ -35,9 +38,9 @@ import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; + +import feign.MethodMetadata; +import feign.Response; import static feign.assertj.FeignAssertions.assertThat; import static java.util.Arrays.asList; @@ -45,84 +48,70 @@ /** * Tests interfaces defined per {@link JAXRSContract} are interpreted into expected {@link feign - * .RequestTemplate template} - * instances. + * .RequestTemplate template} instances. */ public class JAXRSContractTest { - @Rule public final ExpectedException thrown = ExpectedException.none(); - JAXRSContract contract = new JAXRSContract(); - - interface Methods { - @POST void post(); - @PUT void put(); - - @GET void get(); - - @DELETE void delete(); - } + private static final List STRING_LIST = null; + @Rule + public final ExpectedException thrown = ExpectedException.none(); + JAXRSContract contract = new JAXRSContract(); - @Test public void httpMethods() throws Exception { - assertThat(contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("post")).template()) + @Test + public void httpMethods() throws Exception { + assertThat( + contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("post")).template()) .hasMethod("POST"); - assertThat(contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("put")).template()) + assertThat( + contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("put")).template()) .hasMethod("PUT"); - assertThat(contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("get")).template()) + assertThat( + contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("get")).template()) .hasMethod("GET"); - assertThat(contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("delete")).template()) + assertThat( + contract.parseAndValidatateMetadata(Methods.class.getDeclaredMethod("delete")).template()) .hasMethod("DELETE"); } - interface CustomMethod { - @Target({ElementType.METHOD}) - @Retention(RetentionPolicy.RUNTIME) - @HttpMethod("PATCH") - public @interface PATCH { - } - - @PATCH Response patch(); - } - - @Test public void customMethodWithoutPath() throws Exception { - assertThat(contract.parseAndValidatateMetadata(CustomMethod.class.getDeclaredMethod("patch")).template()) + @Test + public void customMethodWithoutPath() throws Exception { + assertThat(contract.parseAndValidatateMetadata(CustomMethod.class.getDeclaredMethod("patch")) + .template()) .hasMethod("PATCH") .hasUrl(""); } - interface WithQueryParamsInPath { - @GET @Path("/") Response none(); - - @GET @Path("/?Action=GetUser") Response one(); - - @GET @Path("/?Action=GetUser&Version=2010-05-08") Response two(); - - @GET @Path("/?Action=GetUser&Version=2010-05-08&limit=1") Response three(); - - @GET @Path("/?flag&Action=GetUser&Version=2010-05-08") Response empty(); - } - - @Test public void queryParamsInPathExtract() throws Exception { - assertThat(contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("none")).template()) + @Test + public void queryParamsInPathExtract() throws Exception { + assertThat( + contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("none")) + .template()) .hasUrl("/") .hasQueries(); - assertThat(contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("one")).template()) + assertThat( + contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("one")) + .template()) .hasUrl("/") .hasQueries( entry("Action", asList("GetUser")) ); - assertThat(contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("two")).template()) + assertThat( + contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("two")) + .template()) .hasUrl("/") .hasQueries( entry("Action", asList("GetUser")), entry("Version", asList("2010-05-08")) ); - assertThat(contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("three")).template()) + assertThat( + contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("three")) + .template()) .hasUrl("/") .hasQueries( entry("Action", asList("GetUser")), @@ -130,82 +119,81 @@ interface WithQueryParamsInPath { entry("limit", asList("1")) ); - assertThat(contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("empty")).template()) + assertThat( + contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("empty")) + .template()) .hasUrl("/") .hasQueries( - entry("flag", asList(new String[] { null })), + entry("flag", asList(new String[]{null})), entry("Action", asList("GetUser")), entry("Version", asList("2010-05-08")) ); } - interface ProducesAndConsumes { - @GET @Produces("application/xml") Response produces(); - - @GET @Produces({}) Response producesNada(); - - @GET @Produces({""}) Response producesEmpty(); - - @POST @Consumes("application/xml") Response consumes(); - - @POST @Consumes({}) Response consumesNada(); - - @POST @Consumes({""}) Response consumesEmpty(); - } - - @Test public void producesAddsAcceptHeader() throws Exception { - MethodMetadata md = contract.parseAndValidatateMetadata(ProducesAndConsumes.class.getDeclaredMethod("produces")); + @Test + public void producesAddsAcceptHeader() throws Exception { + MethodMetadata + md = + contract + .parseAndValidatateMetadata(ProducesAndConsumes.class.getDeclaredMethod("produces")); assertThat(md.template()) .hasHeaders(entry("Accept", asList("application/xml"))); } - @Test public void producesNada() throws Exception { + @Test + public void producesNada() throws Exception { thrown.expect(IllegalStateException.class); thrown.expectMessage("Produces.value() was empty on method producesNada"); - contract.parseAndValidatateMetadata(ProducesAndConsumes.class.getDeclaredMethod("producesNada")); + contract + .parseAndValidatateMetadata(ProducesAndConsumes.class.getDeclaredMethod("producesNada")); } - @Test public void producesEmpty() throws Exception { + @Test + public void producesEmpty() throws Exception { thrown.expect(IllegalStateException.class); thrown.expectMessage("Produces.value() was empty on method producesEmpty"); - contract.parseAndValidatateMetadata(ProducesAndConsumes.class.getDeclaredMethod("producesEmpty")); + contract + .parseAndValidatateMetadata(ProducesAndConsumes.class.getDeclaredMethod("producesEmpty")); } - @Test public void consumesAddsContentTypeHeader() throws Exception { - MethodMetadata md = contract.parseAndValidatateMetadata(ProducesAndConsumes.class.getDeclaredMethod("consumes")); + @Test + public void consumesAddsContentTypeHeader() throws Exception { + MethodMetadata + md = + contract + .parseAndValidatateMetadata(ProducesAndConsumes.class.getDeclaredMethod("consumes")); assertThat(md.template()) .hasHeaders(entry("Content-Type", asList("application/xml"))); } - @Test public void consumesNada() throws Exception { + @Test + public void consumesNada() throws Exception { thrown.expect(IllegalStateException.class); thrown.expectMessage("Consumes.value() was empty on method consumesNada"); - contract.parseAndValidatateMetadata(ProducesAndConsumes.class.getDeclaredMethod("consumesNada")); + contract + .parseAndValidatateMetadata(ProducesAndConsumes.class.getDeclaredMethod("consumesNada")); } - @Test public void consumesEmpty() throws Exception { + @Test + public void consumesEmpty() throws Exception { thrown.expect(IllegalStateException.class); thrown.expectMessage("Consumes.value() was empty on method consumesEmpty"); - contract.parseAndValidatateMetadata(ProducesAndConsumes.class.getDeclaredMethod("consumesEmpty")); + contract + .parseAndValidatateMetadata(ProducesAndConsumes.class.getDeclaredMethod("consumesEmpty")); } - interface BodyParams { - @POST Response post(List body); - - @POST Response tooMany(List body, List body2); - } - - private static final List STRING_LIST = null; - - @Test public void bodyParamIsGeneric() throws Exception { - MethodMetadata md = contract.parseAndValidatateMetadata(BodyParams.class.getDeclaredMethod("post", - List.class)); + @Test + public void bodyParamIsGeneric() throws Exception { + MethodMetadata + md = + contract.parseAndValidatateMetadata(BodyParams.class.getDeclaredMethod("post", + List.class)); assertThat(md.bodyIndex()) .isEqualTo(0); @@ -213,39 +201,29 @@ interface BodyParams { .isEqualTo(getClass().getDeclaredField("STRING_LIST").getGenericType()); } - @Test public void tooManyBodies() throws Exception { + @Test + public void tooManyBodies() throws Exception { thrown.expect(IllegalStateException.class); thrown.expectMessage("Method has too many Body"); - contract.parseAndValidatateMetadata(BodyParams.class.getDeclaredMethod("tooMany", List.class, List.class)); - } - - @Path("") interface EmptyPathOnType { - @GET Response base(); + contract.parseAndValidatateMetadata( + BodyParams.class.getDeclaredMethod("tooMany", List.class, List.class)); } - @Test public void emptyPathOnType() throws Exception { + @Test + public void emptyPathOnType() throws Exception { thrown.expect(IllegalStateException.class); thrown.expectMessage("Path.value() was empty on type "); 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); - } - private MethodMetadata parsePathOnTypeMethod(String name) throws NoSuchMethodException { return contract.parseAndValidatateMetadata(PathOnType.class.getDeclaredMethod(name)); } - @Test public void parsePathMethod() throws Exception { + @Test + public void parsePathMethod() throws Exception { assertThat(parsePathOnTypeMethod("base").template()) .hasUrl("/base"); @@ -253,14 +231,16 @@ private MethodMetadata parsePathOnTypeMethod(String name) throws NoSuchMethodExc .hasUrl("/base/specific"); } - @Test public void emptyPathOnMethod() throws Exception { + @Test + public void emptyPathOnMethod() throws Exception { thrown.expect(IllegalStateException.class); thrown.expectMessage("Path.value() was empty on method emptyPath"); parsePathOnTypeMethod("emptyPath"); } - @Test public void emptyPathParam() throws Exception { + @Test + public void emptyPathParam() throws Exception { thrown.expect(IllegalStateException.class); thrown.expectMessage("PathParam.value() was empty on parameter 0"); @@ -268,11 +248,8 @@ private MethodMetadata parsePathOnTypeMethod(String name) throws NoSuchMethodExc PathOnType.class.getDeclaredMethod("emptyPathParam", String.class)); } - interface WithURIParam { - @GET @Path("/{1}/{2}") Response uriParam(@PathParam("1") String one, URI endpoint, @PathParam("2") String two); - } - - @Test public void withPathAndURIParams() throws Exception { + @Test + public void withPathAndURIParams() throws Exception { MethodMetadata md = contract.parseAndValidatateMetadata( WithURIParam.class.getDeclaredMethod("uriParam", String.class, URI.class, String.class)); @@ -284,43 +261,38 @@ interface WithURIParam { assertThat(md.urlIndex()).isEqualTo(1); } - interface WithPathAndQueryParams { - @GET @Path("/domains/{domainId}/records") - Response recordsByNameAndType(@PathParam("domainId") int id, @QueryParam("name") String nameFilter, - @QueryParam("type") String typeFilter); - - @GET Response empty(@QueryParam("") String empty); - } - - @Test public void pathAndQueryParams() throws Exception { - MethodMetadata md = contract.parseAndValidatateMetadata(WithPathAndQueryParams.class.getDeclaredMethod - ("recordsByNameAndType", int.class, String.class, String.class)); + @Test + public void pathAndQueryParams() throws Exception { + MethodMetadata + md = + contract.parseAndValidatateMetadata(WithPathAndQueryParams.class.getDeclaredMethod + ("recordsByNameAndType", int.class, String.class, String.class)); assertThat(md.template()) .hasQueries(entry("name", asList("{name}")), entry("type", asList("{type}"))); assertThat(md.indexToName()).containsExactly(entry(0, asList("domainId")), - entry(1, asList("name")), entry(2, asList("type"))); + entry(1, asList("name")), + entry(2, asList("type"))); } - @Test public void emptyQueryParam() throws Exception { + @Test + public void emptyQueryParam() throws Exception { thrown.expect(IllegalStateException.class); thrown.expectMessage("QueryParam.value() was empty on parameter 0"); - contract.parseAndValidatateMetadata(WithPathAndQueryParams.class.getDeclaredMethod("empty", String.class)); - } - - interface FormParams { - @POST void login( - @FormParam("customer_name") String customer, - @FormParam("user_name") String user, @FormParam("password") String password); - - @GET Response emptyFormParam(@FormParam("") String empty); + contract.parseAndValidatateMetadata( + WithPathAndQueryParams.class.getDeclaredMethod("empty", String.class)); } - @Test public void formParamsParseIntoIndexToName() throws Exception { - MethodMetadata md = contract.parseAndValidatateMetadata(FormParams.class.getDeclaredMethod("login", String.class, - String.class, String.class)); + @Test + public void formParamsParseIntoIndexToName() throws Exception { + MethodMetadata + md = + contract + .parseAndValidatateMetadata(FormParams.class.getDeclaredMethod("login", String.class, + String.class, + String.class)); assertThat(md.formParams()) .containsExactly("customer_name", "user_name", "password"); @@ -332,30 +304,35 @@ interface FormParams { ); } - /** Body type is only for the body param. */ - @Test public void formParamsDoesNotSetBodyType() throws Exception { - MethodMetadata md = contract.parseAndValidatateMetadata(FormParams.class.getDeclaredMethod("login", String.class, - String.class, String.class)); + /** + * Body type is only for the body param. + */ + @Test + public void formParamsDoesNotSetBodyType() throws Exception { + MethodMetadata + md = + contract + .parseAndValidatateMetadata(FormParams.class.getDeclaredMethod("login", String.class, + String.class, + String.class)); assertThat(md.bodyType()).isNull(); } - @Test public void emptyFormParam() throws Exception { + @Test + public void emptyFormParam() throws Exception { thrown.expect(IllegalStateException.class); thrown.expectMessage("FormParam.value() was empty on parameter 0"); - contract.parseAndValidatateMetadata(FormParams.class.getDeclaredMethod("emptyFormParam", String.class)); - } - - interface HeaderParams { - @POST void logout(@HeaderParam("Auth-Token") String token); - - @GET Response emptyHeaderParam(@HeaderParam("") String empty); + contract.parseAndValidatateMetadata( + FormParams.class.getDeclaredMethod("emptyFormParam", String.class)); } - @Test public void headerParamsParseIntoIndexToName() throws Exception { + @Test + public void headerParamsParseIntoIndexToName() throws Exception { MethodMetadata md = - contract.parseAndValidatateMetadata(HeaderParams.class.getDeclaredMethod("logout", String.class)); + contract.parseAndValidatateMetadata( + HeaderParams.class.getDeclaredMethod("logout", String.class)); assertThat(md.template()) .hasHeaders(entry("Auth-Token", asList("{Auth-Token}"))); @@ -364,41 +341,212 @@ interface HeaderParams { .containsExactly(entry(0, asList("Auth-Token"))); } - @Test public void emptyHeaderParam() throws Exception { + @Test + public void emptyHeaderParam() throws Exception { thrown.expect(IllegalStateException.class); thrown.expectMessage("HeaderParam.value() was empty on parameter 0"); - contract.parseAndValidatateMetadata(HeaderParams.class.getDeclaredMethod("emptyHeaderParam", String.class)); + contract.parseAndValidatateMetadata( + HeaderParams.class.getDeclaredMethod("emptyHeaderParam", String.class)); } - @Path("base") - interface PathsWithoutAnySlashes { - @GET @Path("specific") Response get(); + @Test + public void pathsWithoutSlashesParseCorrectly() throws Exception { + assertThat( + contract.parseAndValidatateMetadata(PathsWithoutAnySlashes.class.getDeclaredMethod("get")) + .template()) + .hasUrl("/base/specific"); } - @Test public void pathsWithoutSlashesParseCorrectly() throws Exception { - assertThat(contract.parseAndValidatateMetadata(PathsWithoutAnySlashes.class.getDeclaredMethod("get")).template()) + @Test + public void pathsWithSomeSlashesParseCorrectly() throws Exception { + assertThat( + contract.parseAndValidatateMetadata(PathsWithSomeSlashes.class.getDeclaredMethod("get")) + .template()) .hasUrl("/base/specific"); } + @Test + public void pathsWithSomeOtherSlashesParseCorrectly() throws Exception { + assertThat(contract.parseAndValidatateMetadata( + PathsWithSomeOtherSlashes.class.getDeclaredMethod("get")).template()) + .hasUrl("/base/specific"); + + } + + interface Methods { + + @POST + void post(); + + @PUT + void put(); + + @GET + void get(); + + @DELETE + void delete(); + } + + interface CustomMethod { + + @PATCH + Response patch(); + + @Target({ElementType.METHOD}) + @Retention(RetentionPolicy.RUNTIME) + @HttpMethod("PATCH") + public @interface PATCH { + + } + } + + interface WithQueryParamsInPath { + + @GET + @Path("/") + Response none(); + + @GET + @Path("/?Action=GetUser") + Response one(); + + @GET + @Path("/?Action=GetUser&Version=2010-05-08") + Response two(); + + @GET + @Path("/?Action=GetUser&Version=2010-05-08&limit=1") + Response three(); + + @GET + @Path("/?flag&Action=GetUser&Version=2010-05-08") + Response empty(); + } + + interface ProducesAndConsumes { + + @GET + @Produces("application/xml") + Response produces(); + + @GET + @Produces({}) + Response producesNada(); + + @GET + @Produces({""}) + Response producesEmpty(); + + @POST + @Consumes("application/xml") + Response consumes(); + + @POST + @Consumes({}) + Response consumesNada(); + + @POST + @Consumes({""}) + Response consumesEmpty(); + } + + interface BodyParams { + + @POST + Response post(List body); + + @POST + Response tooMany(List body, List body2); + } + + @Path("") + interface EmptyPathOnType { + + @GET + Response base(); + } + @Path("/base") - interface PathsWithSomeSlashes { - @GET @Path("specific") Response get(); + 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 pathsWithSomeSlashesParseCorrectly() throws Exception { - assertThat(contract.parseAndValidatateMetadata(PathsWithSomeSlashes.class.getDeclaredMethod("get")).template()) - .hasUrl("/base/specific"); + interface WithURIParam { + + @GET + @Path("/{1}/{2}") + Response uriParam(@PathParam("1") String one, URI endpoint, @PathParam("2") String two); + } + + interface WithPathAndQueryParams { + + @GET + @Path("/domains/{domainId}/records") + Response recordsByNameAndType(@PathParam("domainId") int id, + @QueryParam("name") String nameFilter, + @QueryParam("type") String typeFilter); + + @GET + Response empty(@QueryParam("") String empty); + } + + interface FormParams { + + @POST + void login( + @FormParam("customer_name") String customer, + @FormParam("user_name") String user, @FormParam("password") String password); + + @GET + Response emptyFormParam(@FormParam("") String empty); + } + + interface HeaderParams { + + @POST + void logout(@HeaderParam("Auth-Token") String token); + + @GET + Response emptyHeaderParam(@HeaderParam("") String empty); } @Path("base") - interface PathsWithSomeOtherSlashes { - @GET @Path("/specific") Response get(); + interface PathsWithoutAnySlashes { + + @GET + @Path("specific") + Response get(); } - @Test public void pathsWithSomeOtherSlashesParseCorrectly() throws Exception { - assertThat(contract.parseAndValidatateMetadata(PathsWithSomeOtherSlashes.class.getDeclaredMethod("get")).template()) - .hasUrl("/base/specific"); + @Path("/base") + interface PathsWithSomeSlashes { + + @GET + @Path("specific") + Response get(); + } + + @Path("base") + interface PathsWithSomeOtherSlashes { + @GET + @Path("/specific") + Response get(); } } diff --git a/jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java b/jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java index 2a21e4ddf8..83249ec66f 100644 --- a/jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java +++ b/jaxrs/src/test/java/feign/jaxrs/examples/GitHubExample.java @@ -15,32 +15,24 @@ */ package feign.jaxrs.examples; -import feign.Feign; -import feign.jaxrs.JAXRSContract; import java.util.List; + import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; +import feign.Feign; +import feign.jaxrs.JAXRSContract; + /** * adapted from {@code com.example.retrofit.GitHubClient} */ public class GitHubExample { - interface GitHub { - @GET @Path("/repos/{owner}/{repo}/contributors") - List contributors(@PathParam("owner") String owner, @PathParam("repo") String repo); - } - - static class Contributor { - String login; - int contributions; - } - public static void main(String... args) throws InterruptedException { GitHub github = Feign.builder() - .contract(new JAXRSContract()) - .target(GitHub.class, "https://api.github.com"); + .contract(new JAXRSContract()) + .target(GitHub.class, "https://api.github.com"); System.out.println("Let's fetch and print a list of the contributors to this library."); List contributors = github.contributors("netflix", "feign"); @@ -48,4 +40,18 @@ public static void main(String... args) throws InterruptedException { System.out.println(contributor.login + " (" + contributor.contributions + ")"); } } + + interface GitHub { + + @GET + @Path("/repos/{owner}/{repo}/contributors") + List contributors(@PathParam("owner") String owner, + @PathParam("repo") String repo); + } + + static class Contributor { + + String login; + int contributions; + } } diff --git a/okhttp/src/main/java/feign/okhttp/OkHttpClient.java b/okhttp/src/main/java/feign/okhttp/OkHttpClient.java index 042e6c7846..b3b49f3636 100644 --- a/okhttp/src/main/java/feign/okhttp/OkHttpClient.java +++ b/okhttp/src/main/java/feign/okhttp/OkHttpClient.java @@ -21,7 +21,7 @@ import com.squareup.okhttp.RequestBody; import com.squareup.okhttp.Response; import com.squareup.okhttp.ResponseBody; -import feign.Client; + import java.io.IOException; import java.io.InputStream; import java.io.Reader; @@ -30,14 +30,17 @@ import java.util.Map; import java.util.concurrent.TimeUnit; +import feign.Client; + /** - * This module directs Feign's http requests to OkHttp, which enables - * SPDY and better network control. - * Ex. + * This module directs Feign's http requests to OkHttp, + * which enables SPDY and better network control. Ex. *
- * GitHub github = Feign.builder().client(new OkHttpClient()).target(GitHub.class, "https://api.github.com");
+ * GitHub github = Feign.builder().client(new OkHttpClient()).target(GitHub.class,
+ * "https://api.github.com");
  */
 public final class OkHttpClient implements Client {
+
   private final com.squareup.okhttp.OkHttpClient delegate;
 
   public OkHttpClient() {
@@ -48,21 +51,6 @@ public OkHttpClient(com.squareup.okhttp.OkHttpClient delegate) {
     this.delegate = delegate;
   }
 
-  @Override public feign.Response execute(feign.Request input, feign.Request.Options options) throws IOException {
-    com.squareup.okhttp.OkHttpClient requestScoped;
-    if (delegate.getConnectTimeout() != options.connectTimeoutMillis()
-        || delegate.getReadTimeout() != options.readTimeoutMillis()) {
-      requestScoped = delegate.clone();
-      requestScoped.setConnectTimeout(options.connectTimeoutMillis(), TimeUnit.MILLISECONDS);
-      requestScoped.setReadTimeout(options.readTimeoutMillis(), TimeUnit.MILLISECONDS);
-    } else {
-      requestScoped = delegate;
-    }
-    Request request = toOkHttpRequest(input);
-    Response response = requestScoped.newCall(request).execute();
-    return toFeignResponse(response);
-  }
-
   static Request toOkHttpRequest(feign.Request input) {
     Request.Builder requestBuilder = new Request.Builder();
     requestBuilder.url(input.url());
@@ -70,19 +58,25 @@ static Request toOkHttpRequest(feign.Request input) {
     MediaType mediaType = null;
     boolean hasAcceptHeader = false;
     for (String field : input.headers().keySet()) {
-      if (field.equalsIgnoreCase("Accept")) hasAcceptHeader = true;
+      if (field.equalsIgnoreCase("Accept")) {
+        hasAcceptHeader = true;
+      }
 
       for (String value : input.headers().get(field)) {
         if (field.equalsIgnoreCase("Content-Type")) {
           mediaType = MediaType.parse(value);
-          if (input.charset() != null) mediaType.charset(input.charset());
+          if (input.charset() != null) {
+            mediaType.charset(input.charset());
+          }
         } else {
           requestBuilder.addHeader(field, value);
         }
       }
     }
     // Some servers choke on the default accept string.
-    if (!hasAcceptHeader) requestBuilder.addHeader("Accept", "*/*");
+    if (!hasAcceptHeader) {
+      requestBuilder.addHeader("Accept", "*/*");
+    }
 
     RequestBody body = input.body() != null ? RequestBody.create(mediaType, input.body()) : null;
     requestBuilder.method(input.method(), body);
@@ -90,11 +84,14 @@ static Request toOkHttpRequest(feign.Request input) {
   }
 
   private static feign.Response toFeignResponse(Response input) {
-    return feign.Response.create(input.code(), input.message(), toMap(input.headers()), toBody(input.body()));
+    return feign.Response
+        .create(input.code(), input.message(), toMap(input.headers()), toBody(input.body()));
   }
 
   private static Map> toMap(Headers headers) {
-    Map> result = new LinkedHashMap>(headers.size());
+    Map>
+        result =
+        new LinkedHashMap>(headers.size());
     for (String name : headers.names()) {
       // TODO: this is very inefficient as headers.values iterate case insensitively.
       result.put(name, headers.values(name));
@@ -107,31 +104,53 @@ private static feign.Response.Body toBody(final ResponseBody input) {
       return null;
     }
     if (input.contentLength() > Integer.MAX_VALUE) {
-      throw new UnsupportedOperationException("Length too long "+ input.contentLength());
+      throw new UnsupportedOperationException("Length too long " + input.contentLength());
     }
     final Integer length = input.contentLength() != -1 ? (int) input.contentLength() : null;
 
     return new feign.Response.Body() {
 
-      @Override public void close() throws IOException {
+      @Override
+      public void close() throws IOException {
         input.close();
       }
 
-      @Override public Integer length() {
+      @Override
+      public Integer length() {
         return length;
       }
 
-      @Override public boolean isRepeatable() {
+      @Override
+      public boolean isRepeatable() {
         return false;
       }
 
-      @Override public InputStream asInputStream() throws IOException {
+      @Override
+      public InputStream asInputStream() throws IOException {
         return input.byteStream();
       }
 
-      @Override public Reader asReader() throws IOException {
+      @Override
+      public Reader asReader() throws IOException {
         return input.charStream();
       }
     };
   }
+
+  @Override
+  public feign.Response execute(feign.Request input, feign.Request.Options options)
+      throws IOException {
+    com.squareup.okhttp.OkHttpClient requestScoped;
+    if (delegate.getConnectTimeout() != options.connectTimeoutMillis()
+        || delegate.getReadTimeout() != options.readTimeoutMillis()) {
+      requestScoped = delegate.clone();
+      requestScoped.setConnectTimeout(options.connectTimeoutMillis(), TimeUnit.MILLISECONDS);
+      requestScoped.setReadTimeout(options.readTimeoutMillis(), TimeUnit.MILLISECONDS);
+    } else {
+      requestScoped = delegate;
+    }
+    Request request = toOkHttpRequest(input);
+    Response response = requestScoped.newCall(request).execute();
+    return toFeignResponse(response);
+  }
 }
diff --git a/okhttp/src/test/java/feign/okhttp/OkHttpClientTest.java b/okhttp/src/test/java/feign/okhttp/OkHttpClientTest.java
index 1830c08ff4..ad9ae15818 100644
--- a/okhttp/src/test/java/feign/okhttp/OkHttpClientTest.java
+++ b/okhttp/src/test/java/feign/okhttp/OkHttpClientTest.java
@@ -17,16 +17,19 @@
 
 import com.squareup.okhttp.mockwebserver.MockResponse;
 import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+
 import feign.Feign;
 import feign.FeignException;
 import feign.Headers;
 import feign.RequestLine;
 import feign.Response;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
 
 import static feign.Util.UTF_8;
 import static feign.assertj.MockWebServerAssertions.assertThat;
@@ -34,17 +37,14 @@
 import static org.junit.Assert.assertEquals;
 
 public class OkHttpClientTest {
-  @Rule public final ExpectedException thrown = ExpectedException.none();
-  @Rule public final MockWebServerRule server = new MockWebServerRule();
-
-  interface TestInterface {
-    @RequestLine("POST /?foo=bar&foo=baz&qux=")
-    @Headers({"Foo: Bar", "Foo: Baz", "Qux: ", "Content-Type: text/plain"}) Response post(String body);
 
-    @RequestLine("PATCH /") @Headers("Accept: text/plain") String patch();
-  }
+  @Rule
+  public final ExpectedException thrown = ExpectedException.none();
+  @Rule
+  public final MockWebServerRule server = new MockWebServerRule();
 
-  @Test public void parsesRequestAndResponse() throws IOException, InterruptedException {
+  @Test
+  public void parsesRequestAndResponse() throws IOException, InterruptedException {
     server.enqueue(new MockResponse().setBody("foo").addHeader("Foo: Bar"));
 
     TestInterface api = Feign.builder()
@@ -58,7 +58,8 @@ interface TestInterface {
     assertThat(response.headers())
         .containsEntry("Content-Length", asList("3"))
         .containsEntry("Foo", asList("Bar"));
-    assertThat(response.body().asInputStream()).hasContentEqualTo(new ByteArrayInputStream("foo".getBytes(UTF_8)));
+    assertThat(response.body().asInputStream())
+        .hasContentEqualTo(new ByteArrayInputStream("foo".getBytes(UTF_8)));
 
     assertThat(server.takeRequest()).hasMethod("POST")
         .hasPath("/?foo=bar&foo=baz&qux=")
@@ -66,7 +67,8 @@ interface TestInterface {
         .hasBody("foo");
   }
 
-  @Test public void parsesErrorResponse() throws IOException, InterruptedException {
+  @Test
+  public void parsesErrorResponse() throws IOException, InterruptedException {
     thrown.expect(FeignException.class);
     thrown.expectMessage("status 500 reading TestInterface#post(String); content:\n" + "ARGHH");
 
@@ -79,7 +81,8 @@ interface TestInterface {
     api.post("foo");
   }
 
-  @Test public void patch() throws IOException, InterruptedException {
+  @Test
+  public void patch() throws IOException, InterruptedException {
     server.enqueue(new MockResponse().setBody("foo"));
     server.enqueue(new MockResponse());
 
@@ -94,4 +97,15 @@ interface TestInterface {
         .hasNoHeaderNamed("Content-Type")
         .hasMethod("PATCH");
   }
+
+  interface TestInterface {
+
+    @RequestLine("POST /?foo=bar&foo=baz&qux=")
+    @Headers({"Foo: Bar", "Foo: Baz", "Qux: ", "Content-Type: text/plain"})
+    Response post(String body);
+
+    @RequestLine("PATCH /")
+    @Headers("Accept: text/plain")
+    String patch();
+  }
 }
diff --git a/ribbon/src/main/java/feign/ribbon/LBClient.java b/ribbon/src/main/java/feign/ribbon/LBClient.java
index 83fd602ed6..c3a4ca0d3a 100644
--- a/ribbon/src/main/java/feign/ribbon/LBClient.java
+++ b/ribbon/src/main/java/feign/ribbon/LBClient.java
@@ -24,17 +24,19 @@
 import com.netflix.client.config.CommonClientConfigKey;
 import com.netflix.client.config.IClientConfig;
 import com.netflix.loadbalancer.ILoadBalancer;
-import feign.Client;
-import feign.Request;
-import feign.RequestTemplate;
-import feign.Response;
 
 import java.io.IOException;
 import java.net.URI;
 import java.util.Collection;
 import java.util.Map;
 
-class LBClient extends AbstractLoadBalancerAwareClient {
+import feign.Client;
+import feign.Request;
+import feign.RequestTemplate;
+import feign.Response;
+
+class LBClient
+    extends AbstractLoadBalancerAwareClient {
 
   private final Client delegate;
   private final int connectTimeout;
@@ -51,11 +53,14 @@ class LBClient extends AbstractLoadBalancerAwareClient> getHeaders() {
+    @Override
+    public Map> getHeaders() {
       return response.headers();
     }
 
diff --git a/ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java b/ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java
index d105702754..95162a6a77 100644
--- a/ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java
+++ b/ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java
@@ -30,36 +30,23 @@
 
 /**
  * Basic integration for {@link com.netflix.loadbalancer.ILoadBalancer loadbalancer-aware} targets.
- * Using this will enable dynamic url discovery via ribbon including incrementing server request counts.
- * 
- * Ex. + * Using this will enable dynamic url discovery via ribbon including incrementing server request + * counts.
Ex. *
- * MyService api = Feign.builder().target(LoadBalancingTarget.create(MyService.class, "http://myAppProd"))
+ * MyService api = Feign.builder().target(LoadBalancingTarget.create(MyService.class,
+ * "http://myAppProd"))
  * 
- * Where {@code myAppProd} is the ribbon loadbalancer name and {@code myAppProd.ribbon.listOfServers} configuration - * is set. + * Where {@code myAppProd} is the ribbon loadbalancer name and {@code + * myAppProd.ribbon.listOfServers} configuration is set. * * @param corresponds to {@link feign.Target#type()} */ public class LoadBalancingTarget implements Target { - /** - * creates a target which dynamically derives urls from a {@link com.netflix.loadbalancer.ILoadBalancer loadbalancer}. - * - * @param type corresponds to {@link feign.Target#type()} - * @param schemeName naming convention is {@code https://name} or {@code http://name} where - * name corresponds to {@link com.netflix.client.ClientFactory#getNamedLoadBalancer(String)} - */ - public static LoadBalancingTarget create(Class type, String schemeName) { - URI asUri = URI.create(schemeName); - return new LoadBalancingTarget(type, asUri.getScheme(), asUri.getHost()); - } - private final String name; private final String scheme; private final Class type; private final AbstractLoadBalancer lb; - protected LoadBalancingTarget(Class type, String scheme, String name) { this.type = checkNotNull(type, "type"); this.scheme = checkNotNull(scheme, "scheme"); @@ -67,15 +54,31 @@ protected LoadBalancingTarget(Class type, String scheme, String name) { this.lb = AbstractLoadBalancer.class.cast(getNamedLoadBalancer(name())); } - @Override public Class type() { + /** + * creates a target which dynamically derives urls from a {@link com.netflix.loadbalancer.ILoadBalancer + * loadbalancer}. + * + * @param type corresponds to {@link feign.Target#type()} + * @param schemeName naming convention is {@code https://name} or {@code http://name} where name + * corresponds to {@link com.netflix.client.ClientFactory#getNamedLoadBalancer(String)} + */ + public static LoadBalancingTarget create(Class type, String schemeName) { + URI asUri = URI.create(schemeName); + return new LoadBalancingTarget(type, asUri.getScheme(), asUri.getHost()); + } + + @Override + public Class type() { return type; } - @Override public String name() { + @Override + public String name() { return name; } - @Override public String url() { + @Override + public String url() { return name; } @@ -86,7 +89,8 @@ public AbstractLoadBalancer lb() { return lb; } - @Override public Request apply(RequestTemplate input) { + @Override + public Request apply(RequestTemplate input) { Server currentServer = lb.chooseServer(null); String url = format("%s://%s", scheme, currentServer.getHostPort()); input.insert(0, url); @@ -97,23 +101,26 @@ public AbstractLoadBalancer lb() { } } - @Override public boolean equals(Object obj) { + @Override + public boolean equals(Object obj) { if (obj instanceof LoadBalancingTarget) { LoadBalancingTarget other = (LoadBalancingTarget) obj; return type.equals(other.type) - && name.equals(other.name); + && name.equals(other.name); } return false; } - @Override public int hashCode() { + @Override + public int hashCode() { int result = 17; result = 31 * result + type.hashCode(); result = 31 * result + name.hashCode(); return result; } - @Override public String toString() { + @Override + public String toString() { return "LoadBalancingTarget(type=" + type.getSimpleName() + ", name=" + name + ")"; } } diff --git a/ribbon/src/main/java/feign/ribbon/RibbonClient.java b/ribbon/src/main/java/feign/ribbon/RibbonClient.java index e9abdc7818..1b669d0c57 100644 --- a/ribbon/src/main/java/feign/ribbon/RibbonClient.java +++ b/ribbon/src/main/java/feign/ribbon/RibbonClient.java @@ -4,51 +4,59 @@ import com.netflix.client.ClientFactory; import com.netflix.client.config.IClientConfig; import com.netflix.loadbalancer.ILoadBalancer; + +import java.io.IOException; +import java.net.URI; + import feign.Client; import feign.Request; import feign.Response; -import java.io.IOException; -import java.net.URI; /** - * RibbonClient can be used in Fiegn builder to activate smart routing and resiliency capabilities provided by Ribbon. - * Ex. + * RibbonClient can be used in Fiegn builder to activate smart routing and resiliency capabilities + * provided by Ribbon. Ex. *
- * MyService api = Feign.builder.client(new RibbonClient()).target(MyService.class, "http://myAppProd");
+ * MyService api = Feign.builder.client(new RibbonClient()).target(MyService.class,
+ * "http://myAppProd");
  * 
- * Where {@code myAppProd} is the ribbon client name and {@code myAppProd.ribbon.listOfServers} configuration - * is set. + * Where {@code myAppProd} is the ribbon client name and {@code myAppProd.ribbon.listOfServers} + * configuration is set. */ public class RibbonClient implements Client { - private final Client delegate; + private final Client delegate; - public RibbonClient() { - this.delegate = new Client.Default(null, null); - } + public RibbonClient() { + this.delegate = new Client.Default(null, null); + } - public RibbonClient(Client delegate) { - this.delegate = delegate; - } + public RibbonClient(Client delegate) { + this.delegate = delegate; + } - @Override public Response execute(Request request, Request.Options options) throws IOException { - try { - URI asUri = URI.create(request.url()); - String clientName = asUri.getHost(); - URI uriWithoutSchemeAndPort = URI.create(request.url().replace(asUri.getScheme() + "://" + asUri.getHost(), "")); - LBClient.RibbonRequest ribbonRequest = new LBClient.RibbonRequest(request, uriWithoutSchemeAndPort); - return lbClient(clientName).executeWithLoadBalancer(ribbonRequest).toResponse(); - } catch (ClientException e) { - if (e.getCause() instanceof IOException) { - throw IOException.class.cast(e.getCause()); - } - throw new RuntimeException(e); + @Override + public Response execute(Request request, Request.Options options) throws IOException { + try { + URI asUri = URI.create(request.url()); + String clientName = asUri.getHost(); + URI + uriWithoutSchemeAndPort = + URI.create(request.url().replace(asUri.getScheme() + "://" + asUri.getHost(), "")); + LBClient.RibbonRequest + ribbonRequest = + new LBClient.RibbonRequest(request, uriWithoutSchemeAndPort); + return lbClient(clientName).executeWithLoadBalancer(ribbonRequest).toResponse(); + } catch (ClientException e) { + if (e.getCause() instanceof IOException) { + throw IOException.class.cast(e.getCause()); } + throw new RuntimeException(e); } + } - private LBClient lbClient(String clientName) { - IClientConfig config = ClientFactory.getNamedConfig(clientName); - ILoadBalancer lb = ClientFactory.getNamedLoadBalancer(clientName); - return new LBClient(delegate, lb, config); - } + private LBClient lbClient(String clientName) { + IClientConfig config = ClientFactory.getNamedConfig(clientName); + ILoadBalancer lb = ClientFactory.getNamedLoadBalancer(clientName); + return new LBClient(delegate, lb, config); + } } diff --git a/ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java b/ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java index 79999a8eff..abcf673d34 100644 --- a/ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java +++ b/ribbon/src/test/java/feign/ribbon/LoadBalancingTargetTest.java @@ -17,36 +17,48 @@ import com.squareup.okhttp.mockwebserver.MockResponse; import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule; -import feign.Feign; -import feign.RequestLine; -import java.io.IOException; -import java.net.URL; + import org.junit.Rule; import org.junit.Test; +import java.io.IOException; +import java.net.URL; + +import feign.Feign; +import feign.RequestLine; + import static com.netflix.config.ConfigurationManager.getConfigInstance; import static feign.Util.UTF_8; import static org.junit.Assert.assertEquals; public class LoadBalancingTargetTest { - @Rule public final MockWebServerRule server1 = new MockWebServerRule(); - @Rule public final MockWebServerRule server2 = new MockWebServerRule(); - interface TestInterface { - @RequestLine("POST /") void post(); + @Rule + public final MockWebServerRule server1 = new MockWebServerRule(); + @Rule + public final MockWebServerRule server2 = new MockWebServerRule(); + + static String hostAndPort(URL url) { + // our build slaves have underscores in their hostnames which aren't permitted by ribbon + return "localhost:" + url.getPort(); } - @Test public void loadBalancingDefaultPolicyRoundRobin() throws IOException, InterruptedException { + @Test + public void loadBalancingDefaultPolicyRoundRobin() throws IOException, InterruptedException { String name = "LoadBalancingTargetTest-loadBalancingDefaultPolicyRoundRobin"; String serverListKey = name + ".ribbon.listOfServers"; server1.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8))); server2.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8))); - getConfigInstance().setProperty(serverListKey, hostAndPort(server1.getUrl("")) + "," + hostAndPort(server2.getUrl(""))); + getConfigInstance().setProperty(serverListKey, + hostAndPort(server1.getUrl("")) + "," + hostAndPort( + server2.getUrl(""))); try { - LoadBalancingTarget target = LoadBalancingTarget.create(TestInterface.class, "http://" + name); + LoadBalancingTarget + target = + LoadBalancingTarget.create(TestInterface.class, "http://" + name); TestInterface api = Feign.builder().target(target); api.post(); @@ -61,8 +73,9 @@ interface TestInterface { } } - static String hostAndPort(URL url) { - // our build slaves have underscores in their hostnames which aren't permitted by ribbon - return "localhost:" + url.getPort(); + interface TestInterface { + + @RequestLine("POST /") + void post(); } } diff --git a/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java b/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java index c1e05da961..1c70045201 100644 --- a/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java +++ b/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java @@ -18,36 +18,49 @@ import com.squareup.okhttp.mockwebserver.MockResponse; import com.squareup.okhttp.mockwebserver.SocketPolicy; import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule; -import feign.Feign; -import feign.Param; -import feign.RequestLine; -import java.io.IOException; -import java.net.URL; + import org.junit.After; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; +import java.io.IOException; +import java.net.URL; + +import feign.Feign; +import feign.Param; +import feign.RequestLine; + import static com.netflix.config.ConfigurationManager.getConfigInstance; import static org.junit.Assert.assertEquals; public class RibbonClientTest { - @Rule public final TestName testName = new TestName(); - @Rule public final MockWebServerRule server1 = new MockWebServerRule(); - @Rule public final MockWebServerRule server2 = new MockWebServerRule(); - interface TestInterface { - @RequestLine("POST /") void post(); - @RequestLine("GET /?a={a}") void getWithQueryParameters(@Param("a") String a); + @Rule + public final TestName testName = new TestName(); + @Rule + public final MockWebServerRule server1 = new MockWebServerRule(); + @Rule + public final MockWebServerRule server2 = new MockWebServerRule(); + + static String hostAndPort(URL url) { + // our build slaves have underscores in their hostnames which aren't permitted by ribbon + return "localhost:" + url.getPort(); } - @Test public void loadBalancingDefaultPolicyRoundRobin() throws IOException, InterruptedException { + @Test + public void loadBalancingDefaultPolicyRoundRobin() throws IOException, InterruptedException { server1.enqueue(new MockResponse().setBody("success!")); server2.enqueue(new MockResponse().setBody("success!")); - getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.getUrl("")) + "," + hostAndPort(server2.getUrl(""))); + getConfigInstance().setProperty(serverListKey(), + hostAndPort(server1.getUrl("")) + "," + hostAndPort( + server2.getUrl(""))); - TestInterface api = Feign.builder().client(new RibbonClient()).target(TestInterface.class, "http://" + client()); + TestInterface + api = + Feign.builder().client(new RibbonClient()) + .target(TestInterface.class, "http://" + client()); api.post(); api.post(); @@ -58,13 +71,17 @@ interface TestInterface { // assertEquals(target.lb().getLoadBalancerStats().getSingleServerStat()) } - @Test public void ioExceptionRetry() throws IOException, InterruptedException { + @Test + public void ioExceptionRetry() throws IOException, InterruptedException { server1.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); server1.enqueue(new MockResponse().setBody("success!")); getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.getUrl(""))); - TestInterface api = Feign.builder().client(new RibbonClient()).target(TestInterface.class, "http://" + client()); + TestInterface + api = + Feign.builder().client(new RibbonClient()) + .target(TestInterface.class, "http://" + client()); api.post(); @@ -73,31 +90,36 @@ interface TestInterface { // assertEquals(target.lb().getLoadBalancerStats().getSingleServerStat()) } - /* - This test-case replicates a bug that occurs when using RibbonRequest with a query string. + /* + This test-case replicates a bug that occurs when using RibbonRequest with a query string. - The querystrings would not be URL-encoded, leading to invalid HTTP-requests if the query string contained - invalid characters (ex. space). - */ - @Test public void urlEncodeQueryStringParameters () throws IOException, InterruptedException { - String queryStringValue = "some string with space"; - String expectedQueryStringValue = "some+string+with+space"; - String expectedRequestLine = String.format("GET /?a=%s HTTP/1.1", expectedQueryStringValue); + The querystrings would not be URL-encoded, leading to invalid HTTP-requests if the query string contained + invalid characters (ex. space). + */ + @Test + public void urlEncodeQueryStringParameters() throws IOException, InterruptedException { + String queryStringValue = "some string with space"; + String expectedQueryStringValue = "some+string+with+space"; + String expectedRequestLine = String.format("GET /?a=%s HTTP/1.1", expectedQueryStringValue); - server1.enqueue(new MockResponse().setBody("success!")); + server1.enqueue(new MockResponse().setBody("success!")); - getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.getUrl(""))); + getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.getUrl(""))); - TestInterface api = Feign.builder().client(new RibbonClient()).target(TestInterface.class, "http://" + client()); + TestInterface + api = + Feign.builder().client(new RibbonClient()) + .target(TestInterface.class, "http://" + client()); api.getWithQueryParameters(queryStringValue); final String recordedRequestLine = server1.takeRequest().getRequestLine(); assertEquals(recordedRequestLine, expectedRequestLine); - } + } - @Test public void ioExceptionRetryWithBuilder() throws IOException, InterruptedException { + @Test + public void ioExceptionRetryWithBuilder() throws IOException, InterruptedException { server1.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); server1.enqueue(new MockResponse().setBody("success!")); @@ -114,11 +136,6 @@ invalid characters (ex. space). // assertEquals(target.lb().getLoadBalancerStats().getSingleServerStat()) } - static String hostAndPort(URL url) { - // our build slaves have underscores in their hostnames which aren't permitted by ribbon - return "localhost:" + url.getPort(); - } - private String client() { return testName.getMethodName(); } @@ -127,7 +144,17 @@ private String serverListKey() { return client() + ".ribbon.listOfServers"; } - @After public void clearServerList() { + @After + public void clearServerList() { getConfigInstance().clearProperty(serverListKey()); } + + interface TestInterface { + + @RequestLine("POST /") + void post(); + + @RequestLine("GET /?a={a}") + void getWithQueryParameters(@Param("a") String a); + } } diff --git a/sax/src/main/java/feign/sax/SAXDecoder.java b/sax/src/main/java/feign/sax/SAXDecoder.java index b038f85489..a0af0fd2ad 100644 --- a/sax/src/main/java/feign/sax/SAXDecoder.java +++ b/sax/src/main/java/feign/sax/SAXDecoder.java @@ -15,20 +15,22 @@ */ package feign.sax; -import feign.Response; -import feign.codec.DecodeException; -import feign.codec.Decoder; +import org.xml.sax.ContentHandler; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.XMLReaderFactory; + import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Type; import java.util.LinkedHashMap; import java.util.Map; -import org.xml.sax.ContentHandler; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; -import org.xml.sax.XMLReader; -import org.xml.sax.helpers.XMLReaderFactory; + +import feign.Response; +import feign.codec.DecodeException; +import feign.codec.Decoder; import static feign.Util.checkNotNull; import static feign.Util.checkState; @@ -37,9 +39,7 @@ /** * Decodes responses using SAX, which is supported both in normal JVM environments, as well Android. - *
- *

Basic example with with Feign.Builder

- *
+ *

Basic example with with Feign.Builder


*
  * api = Feign.builder()
  *            .decoder(SAXDecoder.builder()
@@ -51,29 +51,96 @@
  */
 public class SAXDecoder implements Decoder {
 
+  private final Map> handlerFactories;
+
+  private SAXDecoder(Map> handlerFactories) {
+    this.handlerFactories = handlerFactories;
+  }
+
   public static Builder builder() {
     return new Builder();
   }
 
+  @Override
+  public Object decode(Response response, Type type) throws IOException, DecodeException {
+    if (response.body() == null) {
+      return null;
+    }
+    ContentHandlerWithResult.Factory handlerFactory = handlerFactories.get(type);
+    checkState(handlerFactory != null, "type %s not in configured handlers %s", type,
+               handlerFactories.keySet());
+    ContentHandlerWithResult handler = handlerFactory.create();
+    try {
+      XMLReader xmlReader = XMLReaderFactory.createXMLReader();
+      xmlReader.setFeature("http://xml.org/sax/features/namespaces", false);
+      xmlReader.setFeature("http://xml.org/sax/features/validation", false);
+      xmlReader.setContentHandler(handler);
+      InputStream inputStream = response.body().asInputStream();
+      try {
+        xmlReader.parse(new InputSource(inputStream));
+      } finally {
+        ensureClosed(inputStream);
+      }
+      return handler.result();
+    } catch (SAXException e) {
+      throw new DecodeException(e.getMessage(), e);
+    }
+  }
+
+  /**
+   * Implementations are not intended to be shared across requests.
+   */
+  public interface ContentHandlerWithResult extends ContentHandler {
+
+    /**
+     * expected to be set following a call to {@link XMLReader#parse(InputSource)}
+     */
+    T result();
+
+    public interface Factory {
+
+      ContentHandlerWithResult create();
+    }
+  }
+
   public static class Builder {
+
     private final Map> handlerFactories =
         new LinkedHashMap>();
 
     /**
-     * Will call {@link Constructor#newInstance(Object...)} on {@code handlerClass} for each content stream.
-     * 

- *

Note

- *
- * While this is costly vs {@code new}, it may not affect real performance due to the high cost of reading streams. + * Will call {@link Constructor#newInstance(Object...)} on {@code handlerClass} for each content + * stream.

Note


While this is costly vs {@code new}, it may not affect real + * performance due to the high cost of reading streams. * * @throws IllegalArgumentException if there's no no-arg constructor on {@code handlerClass}. */ - public > Builder registerContentHandler(Class handlerClass) { - Type type = resolveLastTypeParameter(checkNotNull(handlerClass, "handlerClass"), ContentHandlerWithResult.class); - return registerContentHandler(type, new NewInstanceContentHandlerWithResultFactory(handlerClass)); + public > Builder registerContentHandler( + Class handlerClass) { + Type + type = + resolveLastTypeParameter(checkNotNull(handlerClass, "handlerClass"), + ContentHandlerWithResult.class); + return registerContentHandler(type, + new NewInstanceContentHandlerWithResultFactory(handlerClass)); } - private static class NewInstanceContentHandlerWithResultFactory implements ContentHandlerWithResult.Factory { + /** + * Will call {@link ContentHandlerWithResult.Factory#create()} on {@code handler} for each + * content stream. The {@code handler} is expected to have a generic parameter of {@code type}. + */ + public Builder registerContentHandler(Type type, ContentHandlerWithResult.Factory handler) { + this.handlerFactories.put(checkNotNull(type, "type"), checkNotNull(handler, "handler")); + return this; + } + + public SAXDecoder build() { + return new SAXDecoder(handlerFactories); + } + + private static class NewInstanceContentHandlerWithResultFactory + implements ContentHandlerWithResult.Factory { + private final Constructor> ctor; private NewInstanceContentHandlerWithResultFactory(Class> clazz) { @@ -86,7 +153,8 @@ private NewInstanceContentHandlerWithResultFactory(Class create() { + @Override + public ContentHandlerWithResult create() { try { return ctor.newInstance(); } catch (Exception e) { @@ -94,64 +162,5 @@ private NewInstanceContentHandlerWithResultFactory(Class handler) { - this.handlerFactories.put(checkNotNull(type, "type"), checkNotNull(handler, "handler")); - return this; - } - - public SAXDecoder build() { - return new SAXDecoder(handlerFactories); - } - } - - /** - * Implementations are not intended to be shared across requests. - */ - public interface ContentHandlerWithResult extends ContentHandler { - - public interface Factory { - ContentHandlerWithResult create(); - } - - /** - * expected to be set following a call to {@link XMLReader#parse(InputSource)} - */ - T result(); - } - - private final Map> handlerFactories; - - private SAXDecoder(Map> handlerFactories) { - this.handlerFactories = handlerFactories; - } - - @Override - public Object decode(Response response, Type type) throws IOException, DecodeException { - if (response.body() == null) { - return null; - } - ContentHandlerWithResult.Factory handlerFactory = handlerFactories.get(type); - checkState(handlerFactory != null, "type %s not in configured handlers %s", type, handlerFactories.keySet()); - ContentHandlerWithResult handler = handlerFactory.create(); - try { - XMLReader xmlReader = XMLReaderFactory.createXMLReader(); - xmlReader.setFeature("http://xml.org/sax/features/namespaces", false); - xmlReader.setFeature("http://xml.org/sax/features/validation", false); - xmlReader.setContentHandler(handler); - InputStream inputStream = response.body().asInputStream(); - try { - xmlReader.parse(new InputSource(inputStream)); - } finally { - ensureClosed(inputStream); - } - return handler.result(); - } catch (SAXException e) { - throw new DecodeException(e.getMessage(), e); - } } } diff --git a/sax/src/test/java/feign/sax/SAXDecoderTest.java b/sax/src/test/java/feign/sax/SAXDecoderTest.java index 903eb60b3c..063018ab7e 100644 --- a/sax/src/test/java/feign/sax/SAXDecoderTest.java +++ b/sax/src/test/java/feign/sax/SAXDecoderTest.java @@ -15,39 +15,57 @@ */ package feign.sax; -import feign.Response; -import feign.codec.Decoder; -import java.io.IOException; -import java.text.ParseException; -import java.util.Collection; -import java.util.Collections; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.xml.sax.helpers.DefaultHandler; +import java.io.IOException; +import java.text.ParseException; +import java.util.Collection; +import java.util.Collections; + +import feign.Response; +import feign.codec.Decoder; + import static feign.Util.UTF_8; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; public class SAXDecoderTest { - @Rule public final ExpectedException thrown = ExpectedException.none(); + static String statusFailed = ""// + + "\n" +// + + " \n"// + + " \n" +// + + " Failed\n" +// + + " \n"// + + " \n"// + + ""; + @Rule + public final ExpectedException thrown = ExpectedException.none(); Decoder decoder = SAXDecoder.builder() // - .registerContentHandler(NetworkStatus.class, new SAXDecoder.ContentHandlerWithResult.Factory() { - @Override public SAXDecoder.ContentHandlerWithResult create() { - return new NetworkStatusHandler(); - } - }) // + .registerContentHandler(NetworkStatus.class, + new SAXDecoder.ContentHandlerWithResult.Factory() { + @Override + public SAXDecoder.ContentHandlerWithResult create() { + return new NetworkStatusHandler(); + } + }) // .registerContentHandler(NetworkStatusStringHandler.class) // .build(); - @Test public void parsesConfiguredTypes() throws ParseException, IOException { + @Test + public void parsesConfiguredTypes() throws ParseException, IOException { assertEquals(NetworkStatus.FAILED, decoder.decode(statusFailedResponse(), NetworkStatus.class)); assertEquals("Failed", decoder.decode(statusFailedResponse(), String.class)); } - @Test public void niceErrorOnUnconfiguredType() throws ParseException, IOException { + @Test + public void niceErrorOnUnconfiguredType() throws ParseException, IOException { thrown.expect(IllegalStateException.class); thrown.expectMessage("type int not in configured handlers"); @@ -55,24 +73,25 @@ public class SAXDecoderTest { } private Response statusFailedResponse() { - return Response.create(200, "OK", Collections.>emptyMap(), statusFailed, UTF_8); + return Response + .create(200, "OK", Collections.>emptyMap(), statusFailed, UTF_8); } - static String statusFailed = ""// - + "\n"// - + " \n"// - + " \n"// - + " Failed\n"// - + " \n"// - + " \n"// - + ""; + @Test + public void nullBodyDecodesToNull() throws Exception { + Response + response = + Response + .create(204, "OK", Collections.>emptyMap(), (byte[]) null); + assertNull(decoder.decode(response, String.class)); + } static enum NetworkStatus { GOOD, FAILED; } static class NetworkStatusStringHandler extends DefaultHandler implements - SAXDecoder.ContentHandlerWithResult { + SAXDecoder.ContentHandlerWithResult { private StringBuilder currentText = new StringBuilder(); @@ -98,7 +117,7 @@ public void characters(char ch[], int start, int length) { } static class NetworkStatusHandler extends DefaultHandler implements - SAXDecoder.ContentHandlerWithResult { + SAXDecoder.ContentHandlerWithResult { private StringBuilder currentText = new StringBuilder(); @@ -122,9 +141,4 @@ public void characters(char ch[], int start, int length) { currentText.append(ch, start, length); } } - - @Test public void nullBodyDecodesToNull() throws Exception { - Response response = Response.create(204, "OK", Collections.>emptyMap(), (byte[]) null); - assertNull(decoder.decode(response, String.class)); - } } diff --git a/sax/src/test/java/feign/sax/examples/AWSSignatureVersion4.java b/sax/src/test/java/feign/sax/examples/AWSSignatureVersion4.java index 53b2671f92..60dd84945d 100644 --- a/sax/src/test/java/feign/sax/examples/AWSSignatureVersion4.java +++ b/sax/src/test/java/feign/sax/examples/AWSSignatureVersion4.java @@ -15,21 +15,30 @@ */ package feign.sax.examples; -import feign.Request; -import feign.RequestTemplate; import java.net.URI; import java.security.MessageDigest; import java.text.SimpleDateFormat; import java.util.Date; import java.util.TimeZone; + import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; +import feign.Request; +import feign.RequestTemplate; + import static feign.Util.UTF_8; // http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html public class AWSSignatureVersion4 { + private static final String + EMPTY_STRING_HASH = + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + private static final SimpleDateFormat iso8601 = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"); + static { + iso8601.setTimeZone(TimeZone.getTimeZone("GMT")); + } String region = "us-east-1"; String service = "iam"; String accessKey; @@ -40,45 +49,6 @@ public AWSSignatureVersion4(String accessKey, String secretKey) { this.secretKey = secretKey; } - public Request apply(RequestTemplate input) { - if (!input.headers().isEmpty()) throw new UnsupportedOperationException("headers not supported"); - if (input.body() != null) throw new UnsupportedOperationException("body not supported"); - - String host = URI.create(input.url()).getHost(); - - String timestamp; - synchronized (iso8601) { - timestamp = iso8601.format(new Date()); - } - - String credentialScope = String.format("%s/%s/%s/%s", timestamp.substring(0, 8), region, service, "aws4_request"); - - input.query("X-Amz-Algorithm", "AWS4-HMAC-SHA256"); - input.query("X-Amz-Credential", accessKey + "/" + credentialScope); - input.query("X-Amz-Date", timestamp); - input.query("X-Amz-SignedHeaders", "host"); - input.header("Host", host); - - String canonicalString = canonicalString(input, host); - String toSign = toSign(timestamp, credentialScope, canonicalString); - - byte[] signatureKey = signatureKey(secretKey, timestamp); - String signature = hex(hmacSHA256(toSign, signatureKey)); - - input.query("X-Amz-Signature", signature); - - return input.request(); - } - - byte[] signatureKey(String secretKey, String timestamp) { - byte[] kSecret = ("AWS4" + secretKey).getBytes(UTF_8); - byte[] kDate = hmacSHA256(timestamp.substring(0, 8), kSecret); - byte[] kRegion = hmacSHA256(region, kDate); - byte[] kService = hmacSHA256(service, kRegion); - byte[] kSigning = hmacSHA256("aws4_request", kService); - return kSigning; - } - static byte[] hmacSHA256(String data, byte[] key) { try { String algorithm = "HmacSHA256"; @@ -90,8 +60,6 @@ static byte[] hmacSHA256(String data, byte[] key) { } } - private static final String EMPTY_STRING_HASH = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; - private static String canonicalString(RequestTemplate input, String host) { StringBuilder canonicalRequest = new StringBuilder(); // HTTPRequestMethod + '\n' + @@ -114,7 +82,8 @@ private static String canonicalString(RequestTemplate input, String host) { // HexEncode(Hash(Payload)) String bodyText = - input.charset() != null && input.body() != null ? new String(input.body(), input.charset()) : null; + input.charset() != null && input.body() != null ? new String(input.body(), input.charset()) + : null; if (bodyText != null) { canonicalRequest.append(hex(sha256(bodyText))); } else { @@ -154,9 +123,48 @@ static byte[] sha256(String data) { } } - private static final SimpleDateFormat iso8601 = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"); + public Request apply(RequestTemplate input) { + if (!input.headers().isEmpty()) { + throw new UnsupportedOperationException("headers not supported"); + } + if (input.body() != null) { + throw new UnsupportedOperationException("body not supported"); + } - static { - iso8601.setTimeZone(TimeZone.getTimeZone("GMT")); + String host = URI.create(input.url()).getHost(); + + String timestamp; + synchronized (iso8601) { + timestamp = iso8601.format(new Date()); + } + + String + credentialScope = + String.format("%s/%s/%s/%s", timestamp.substring(0, 8), region, service, "aws4_request"); + + input.query("X-Amz-Algorithm", "AWS4-HMAC-SHA256"); + input.query("X-Amz-Credential", accessKey + "/" + credentialScope); + input.query("X-Amz-Date", timestamp); + input.query("X-Amz-SignedHeaders", "host"); + input.header("Host", host); + + String canonicalString = canonicalString(input, host); + String toSign = toSign(timestamp, credentialScope, canonicalString); + + byte[] signatureKey = signatureKey(secretKey, timestamp); + String signature = hex(hmacSHA256(toSign, signatureKey)); + + input.query("X-Amz-Signature", signature); + + return input.request(); + } + + byte[] signatureKey(String secretKey, String timestamp) { + byte[] kSecret = ("AWS4" + secretKey).getBytes(UTF_8); + byte[] kDate = hmacSHA256(timestamp.substring(0, 8), kSecret); + byte[] kRegion = hmacSHA256(region, kDate); + byte[] kService = hmacSHA256(service, kRegion); + byte[] kSigning = hmacSHA256("aws4_request", kService); + return kSigning; } } diff --git a/sax/src/test/java/feign/sax/examples/IAMExample.java b/sax/src/test/java/feign/sax/examples/IAMExample.java index e00b7be493..decf57fd54 100644 --- a/sax/src/test/java/feign/sax/examples/IAMExample.java +++ b/sax/src/test/java/feign/sax/examples/IAMExample.java @@ -15,20 +15,17 @@ */ package feign.sax.examples; +import org.xml.sax.helpers.DefaultHandler; + import feign.Feign; import feign.Request; import feign.RequestLine; import feign.RequestTemplate; import feign.Target; import feign.sax.SAXDecoder; -import org.xml.sax.helpers.DefaultHandler; public class IAMExample { - interface IAM { - @RequestLine("GET /?Action=GetUser&Version=2010-05-08") Long userId(); - } - public static void main(String... args) { IAM iam = Feign.builder()// .decoder(SAXDecoder.builder().registerContentHandler(UserIdHandler.class).build())// @@ -36,31 +33,42 @@ public static void main(String... args) { System.out.println(iam.userId()); } + interface IAM { + + @RequestLine("GET /?Action=GetUser&Version=2010-05-08") + Long userId(); + } + static class IAMTarget extends AWSSignatureVersion4 implements Target { - @Override public Class type() { + private IAMTarget(String accessKey, String secretKey) { + super(accessKey, secretKey); + } + + @Override + public Class type() { return IAM.class; } - @Override public String name() { + @Override + public String name() { return "iam"; } - @Override public String url() { + @Override + public String url() { return "https://iam.amazonaws.com"; } - private IAMTarget(String accessKey, String secretKey) { - super(accessKey, secretKey); - } - - @Override public Request apply(RequestTemplate in) { + @Override + public Request apply(RequestTemplate in) { in.insert(0, url()); return super.apply(in); } } - static class UserIdHandler extends DefaultHandler implements SAXDecoder.ContentHandlerWithResult { + static class UserIdHandler extends DefaultHandler + implements SAXDecoder.ContentHandlerWithResult { private StringBuilder currentText = new StringBuilder(); diff --git a/slf4j/src/main/java/feign/slf4j/Slf4jLogger.java b/slf4j/src/main/java/feign/slf4j/Slf4jLogger.java index 724d7c60ba..90888c4ffb 100644 --- a/slf4j/src/main/java/feign/slf4j/Slf4jLogger.java +++ b/slf4j/src/main/java/feign/slf4j/Slf4jLogger.java @@ -15,18 +15,21 @@ */ package feign.slf4j; -import feign.Request; -import feign.Response; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; +import feign.Request; +import feign.Response; + /** - * Logs to SLF4J at the debug level, if the underlying logger has debug logging enabled. The underlying logger can - * be specified at construction-time, defaulting to the logger for {@link feign.Logger}. + * Logs to SLF4J at the debug level, if the underlying logger has debug logging enabled. The + * underlying logger can be specified at construction-time, defaulting to the logger for {@link + * feign.Logger}. */ public class Slf4jLogger extends feign.Logger { + private final Logger logger; public Slf4jLogger() { @@ -45,20 +48,24 @@ public Slf4jLogger(String name) { this.logger = logger; } - @Override protected void logRequest(String configKey, Level logLevel, Request request) { + @Override + protected void logRequest(String configKey, Level logLevel, Request request) { if (logger.isDebugEnabled()) { super.logRequest(configKey, logLevel, request); } } - @Override protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime) throws IOException { + @Override + protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response, + long elapsedTime) throws IOException { if (logger.isDebugEnabled()) { return super.logAndRebufferResponse(configKey, logLevel, response, elapsedTime); } return response; } - @Override protected void log(String configKey, String format, Object... args) { + @Override + protected void log(String configKey, String format, Object... args) { // Not using SLF4J's support for parameterized messages (even though it would be more efficient) because it would // require the incoming message formats to be SLF4J-specific. logger.debug(String.format(methodTag(configKey) + format, args)); diff --git a/slf4j/src/test/java/feign/slf4j/RecordingSimpleLogger.java b/slf4j/src/test/java/feign/slf4j/RecordingSimpleLogger.java index 9525c87e1b..ae6919e278 100644 --- a/slf4j/src/test/java/feign/slf4j/RecordingSimpleLogger.java +++ b/slf4j/src/test/java/feign/slf4j/RecordingSimpleLogger.java @@ -15,10 +15,6 @@ */ package feign.slf4j; -import java.io.ByteArrayOutputStream; -import java.io.PrintStream; -import java.lang.reflect.Field; -import java.lang.reflect.Method; import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; @@ -26,19 +22,26 @@ import org.slf4j.impl.SimpleLogger; import org.slf4j.impl.SimpleLoggerFactory; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + import static org.junit.Assert.assertEquals; import static org.slf4j.impl.SimpleLogger.DEFAULT_LOG_LEVEL_KEY; import static org.slf4j.impl.SimpleLogger.SHOW_THREAD_NAME_KEY; /** - * A testing utility to allow control over {@link org.slf4j.impl.SimpleLogger}. - * In some cases, reflection is used to bypass access restrictions. + * A testing utility to allow control over {@link org.slf4j.impl.SimpleLogger}. In some cases, + * reflection is used to bypass access restrictions. */ final class RecordingSimpleLogger implements TestRule { private String expectedMessages = ""; - /** Resets {@link org.slf4j.impl.SimpleLogger} to the new log level. */ + /** + * Resets {@link org.slf4j.impl.SimpleLogger} to the new log level. + */ RecordingSimpleLogger logLevel(String logLevel) throws Exception { System.setProperty(SHOW_THREAD_NAME_KEY, "false"); System.setProperty(DEFAULT_LOG_LEVEL_KEY, logLevel); @@ -53,16 +56,22 @@ RecordingSimpleLogger logLevel(String logLevel) throws Exception { return this; } - /** Newline delimited output that would be sent to stderr. */ + /** + * Newline delimited output that would be sent to stderr. + */ RecordingSimpleLogger expectMessages(String expectedMessages) { this.expectedMessages = expectedMessages; return this; } - /** Steals the output of stderr as that's where the log events go. */ - @Override public Statement apply(final Statement base, Description description) { + /** + * Steals the output of stderr as that's where the log events go. + */ + @Override + public Statement apply(final Statement base, Description description) { return new Statement() { - @Override public void evaluate() throws Throwable { + @Override + public void evaluate() throws Throwable { ByteArrayOutputStream buff = new ByteArrayOutputStream(); PrintStream stderr = System.err; try { diff --git a/slf4j/src/test/java/feign/slf4j/Slf4jLoggerTest.java b/slf4j/src/test/java/feign/slf4j/Slf4jLoggerTest.java index b81560bd4c..dc9d6ab457 100644 --- a/slf4j/src/test/java/feign/slf4j/Slf4jLoggerTest.java +++ b/slf4j/src/test/java/feign/slf4j/Slf4jLoggerTest.java @@ -15,29 +15,32 @@ */ package feign.slf4j; +import org.junit.Rule; +import org.junit.Test; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.Collections; + import feign.Feign; import feign.Logger; import feign.Request; import feign.RequestTemplate; import feign.Response; -import java.util.Collection; -import java.util.Collections; -import org.junit.Rule; -import org.junit.Test; -import org.slf4j.LoggerFactory; public class Slf4jLoggerTest { - @Rule public final RecordingSimpleLogger slf4j = new RecordingSimpleLogger(); private static final String CONFIG_KEY = "someMethod()"; private static final Request REQUEST = new RequestTemplate().method("GET").append("http://api.example.com").request(); private static final Response RESPONSE = Response.create(200, "OK", Collections.>emptyMap(), new byte[0]); - + @Rule + public final RecordingSimpleLogger slf4j = new RecordingSimpleLogger(); private Slf4jLogger logger; - @Test public void useFeignLoggerByDefault() throws Exception { + @Test + public void useFeignLoggerByDefault() throws Exception { slf4j.logLevel("debug"); slf4j.expectMessages("DEBUG feign.Logger - [someMethod] This is my message\n"); @@ -45,7 +48,8 @@ public class Slf4jLoggerTest { logger.log(CONFIG_KEY, "This is my message"); } - @Test public void useLoggerByNameIfRequested() throws Exception { + @Test + public void useLoggerByNameIfRequested() throws Exception { slf4j.logLevel("debug"); slf4j.expectMessages("DEBUG named.logger - [someMethod] This is my message\n"); @@ -53,7 +57,8 @@ public class Slf4jLoggerTest { logger.log(CONFIG_KEY, "This is my message"); } - @Test public void useLoggerByClassIfRequested() throws Exception { + @Test + public void useLoggerByClassIfRequested() throws Exception { slf4j.logLevel("debug"); slf4j.expectMessages("DEBUG feign.Feign - [someMethod] This is my message\n"); @@ -61,7 +66,8 @@ public class Slf4jLoggerTest { logger.log(CONFIG_KEY, "This is my message"); } - @Test public void useSpecifiedLoggerIfRequested() throws Exception { + @Test + public void useSpecifiedLoggerIfRequested() throws Exception { slf4j.logLevel("debug"); slf4j.expectMessages("DEBUG specified.logger - [someMethod] This is my message\n"); @@ -69,7 +75,8 @@ public class Slf4jLoggerTest { logger.log(CONFIG_KEY, "This is my message"); } - @Test public void logOnlyIfDebugEnabled() throws Exception { + @Test + public void logOnlyIfDebugEnabled() throws Exception { slf4j.logLevel("info"); logger = new Slf4jLogger(); @@ -78,11 +85,13 @@ public class Slf4jLoggerTest { logger.logAndRebufferResponse(CONFIG_KEY, Logger.Level.BASIC, RESPONSE, 273); } - @Test public void logRequestsAndResponses() throws Exception { + @Test + public void logRequestsAndResponses() throws Exception { slf4j.logLevel("debug"); slf4j.expectMessages("DEBUG feign.Logger - [someMethod] A message with 2 formatting tokens.\n" + - "DEBUG feign.Logger - [someMethod] ---> GET http://api.example.com HTTP/1.1\n" + - "DEBUG feign.Logger - [someMethod] <--- HTTP/1.1 200 OK (273ms)\n"); + "DEBUG feign.Logger - [someMethod] ---> GET http://api.example.com HTTP/1.1\n" + + + "DEBUG feign.Logger - [someMethod] <--- HTTP/1.1 200 OK (273ms)\n"); logger = new Slf4jLogger(); logger.log(CONFIG_KEY, "A message with %d formatting %s.", 2, "tokens"); From 74e23ef80864f1930adc198b203b795cb0c44707 Mon Sep 17 00:00:00 2001 From: jdamick Date: Wed, 4 Feb 2015 16:00:21 -0500 Subject: [PATCH 170/179] Headers substitutions were not being expanded by the value name, instead it was using the header name.. --- core/src/main/java/feign/RequestTemplate.java | 2 +- core/src/test/java/feign/DefaultContractTest.java | 8 ++++---- core/src/test/java/feign/RequestTemplateTest.java | 11 +++++++++++ 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/feign/RequestTemplate.java b/core/src/main/java/feign/RequestTemplate.java index 2574f0c2a7..607a90b3b2 100644 --- a/core/src/main/java/feign/RequestTemplate.java +++ b/core/src/main/java/feign/RequestTemplate.java @@ -205,7 +205,7 @@ public RequestTemplate resolve(Map unencoded) { for (String value : valuesOrEmpty(headers, field)) { String resolved; if (value.indexOf('{') == 0) { - resolved = String.valueOf(unencoded.get(field)); + resolved = expand(value, unencoded); } else { resolved = value; } diff --git a/core/src/test/java/feign/DefaultContractTest.java b/core/src/test/java/feign/DefaultContractTest.java index 607244e6a9..99b1b7a727 100644 --- a/core/src/test/java/feign/DefaultContractTest.java +++ b/core/src/test/java/feign/DefaultContractTest.java @@ -245,10 +245,10 @@ public void headerParamsParseIntoIndexToName() throws Exception { HeaderParams.class.getDeclaredMethod("logout", String.class)); assertThat(md.template()) - .hasHeaders(entry("Auth-Token", asList("{Auth-Token}", "Foo"))); + .hasHeaders(entry("Auth-Token", asList("{authToken}", "Foo"))); assertThat(md.indexToName()) - .containsExactly(entry(0, asList("Auth-Token"))); + .containsExactly(entry(0, asList("authToken"))); } @Test @@ -343,8 +343,8 @@ void login( interface HeaderParams { @RequestLine("POST /") - @Headers({"Auth-Token: {Auth-Token}", "Auth-Token: Foo"}) - void logout(@Param("Auth-Token") String token); + @Headers({"Auth-Token: {authToken}", "Auth-Token: Foo"}) + void logout(@Param("authToken") String token); } interface CustomExpander { diff --git a/core/src/test/java/feign/RequestTemplateTest.java b/core/src/test/java/feign/RequestTemplateTest.java index a4893f7ea2..5e7481c30f 100644 --- a/core/src/test/java/feign/RequestTemplateTest.java +++ b/core/src/test/java/feign/RequestTemplateTest.java @@ -126,6 +126,17 @@ public void resolveTemplateWithBaseAndParameterizedIterableQuery() { ); } + @Test + public void resolveTemplateWithHeaderSubstitutions() { + RequestTemplate template = new RequestTemplate().method("GET") + .header("Auth-Token", "{authToken}"); + + template.resolve(mapOf("authToken", "1234")); + + assertThat(template) + .hasHeaders(entry("Auth-Token", asList("1234"))); + } + @Test public void resolveTemplateWithMixedRequestLineParams() throws Exception { RequestTemplate template = new RequestTemplate().method("GET")// From 59a159e05842ba8c37510aa65afba3e870d080f8 Mon Sep 17 00:00:00 2001 From: Brendan Nolan Date: Fri, 6 Feb 2015 19:16:47 +0000 Subject: [PATCH 171/179] Adds Request.Options support to RibbonClient --- CHANGELOG.md | 3 ++ .../main/java/feign/ribbon/RibbonClient.java | 25 ++++++++++++++- .../java/feign/ribbon/RibbonClientTest.java | 31 ++++++++++++++----- 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef53183823..5269a2ccba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ * Removes support for parameters annotated with `javax.inject.@Named`. Use `feign.@Param` instead. * Makes body parameter type explicit. +### Version 7.3 +* Adds Request.Options support to RibbonClient + ### Version 7.2 * Adds `Feign.Builder.build()` * Opens constructor for Gson and Jackson codecs which accepts type adapters diff --git a/ribbon/src/main/java/feign/ribbon/RibbonClient.java b/ribbon/src/main/java/feign/ribbon/RibbonClient.java index 1b669d0c57..263ac00ac3 100644 --- a/ribbon/src/main/java/feign/ribbon/RibbonClient.java +++ b/ribbon/src/main/java/feign/ribbon/RibbonClient.java @@ -2,6 +2,8 @@ import com.netflix.client.ClientException; import com.netflix.client.ClientFactory; +import com.netflix.client.config.CommonClientConfigKey; +import com.netflix.client.config.DefaultClientConfigImpl; import com.netflix.client.config.IClientConfig; import com.netflix.loadbalancer.ILoadBalancer; @@ -45,7 +47,8 @@ public Response execute(Request request, Request.Options options) throws IOExcep LBClient.RibbonRequest ribbonRequest = new LBClient.RibbonRequest(request, uriWithoutSchemeAndPort); - return lbClient(clientName).executeWithLoadBalancer(ribbonRequest).toResponse(); + return lbClient(clientName).executeWithLoadBalancer(ribbonRequest, + new FeignOptionsClientConfig(options)).toResponse(); } catch (ClientException e) { if (e.getCause() instanceof IOException) { throw IOException.class.cast(e.getCause()); @@ -59,4 +62,24 @@ private LBClient lbClient(String clientName) { ILoadBalancer lb = ClientFactory.getNamedLoadBalancer(clientName); return new LBClient(delegate, lb, config); } + + static class FeignOptionsClientConfig extends DefaultClientConfigImpl { + + public FeignOptionsClientConfig(Request.Options options) { + setProperty(CommonClientConfigKey.ConnectTimeout, options.connectTimeoutMillis()); + setProperty(CommonClientConfigKey.ReadTimeout, options.readTimeoutMillis()); + } + + @Override + public void loadProperties(String clientName) { + + } + + @Override + public void loadDefaultValues() { + + } + + } + } diff --git a/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java b/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java index 1c70045201..7faed74394 100644 --- a/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java +++ b/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java @@ -15,25 +15,30 @@ */ package feign.ribbon; -import com.squareup.okhttp.mockwebserver.MockResponse; -import com.squareup.okhttp.mockwebserver.SocketPolicy; -import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule; +import static com.netflix.config.ConfigurationManager.getConfigInstance; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +import java.io.IOException; +import java.net.URL; import org.junit.After; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; -import java.io.IOException; -import java.net.URL; +import com.netflix.client.config.CommonClientConfigKey; +import com.netflix.client.config.IClientConfig; +import com.squareup.okhttp.mockwebserver.MockResponse; +import com.squareup.okhttp.mockwebserver.SocketPolicy; +import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule; import feign.Feign; import feign.Param; +import feign.Request; import feign.RequestLine; -import static com.netflix.config.ConfigurationManager.getConfigInstance; -import static org.junit.Assert.assertEquals; - public class RibbonClientTest { @Rule @@ -135,6 +140,16 @@ public void ioExceptionRetryWithBuilder() throws IOException, InterruptedExcepti // TODO: verify ribbon stats match // assertEquals(target.lb().getLoadBalancerStats().getSingleServerStat()) } + + @Test + public void testFeignOptionsClientConfig() { + Request.Options options = new Request.Options(1111, 22222); + IClientConfig config = new RibbonClient.FeignOptionsClientConfig(options); + assertThat(config.get(CommonClientConfigKey.ConnectTimeout), + equalTo(options.connectTimeoutMillis())); + assertThat(config.get(CommonClientConfigKey.ReadTimeout), equalTo(options.readTimeoutMillis())); + assertEquals(2, config.getProperties().size()); + } private String client() { return testName.getMethodName(); From c53b1773ae7d4f2ba1cc53941f7e2470b32c5671 Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Sat, 7 Feb 2015 10:08:55 -0800 Subject: [PATCH 172/179] Updates examples to Feign 7.2.1 --- example-github/README.md | 4 ++-- example-github/build.gradle | 4 ++-- example-github/pom.xml | 2 +- example-wikipedia/build.gradle | 4 ++-- example-wikipedia/pom.xml | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/example-github/README.md b/example-github/README.md index 6070b912b2..d6ae311657 100644 --- a/example-github/README.md +++ b/example-github/README.md @@ -4,7 +4,7 @@ GitHub Example This is an example of a simple json client. === Building example with Gradle -Install and run `gradle` to produce `build/wikipedia` +Install and run `gradle` to produce `build/github` === Building example with Maven -Install and run `mvn` to produce `target/wikipedia` +Install and run `mvn` to produce `target/github` diff --git a/example-github/build.gradle b/example-github/build.gradle index c9558f24f1..92b26920a9 100644 --- a/example-github/build.gradle +++ b/example-github/build.gradle @@ -12,8 +12,8 @@ configurations { } dependencies { - compile 'com.netflix.feign:feign-core:7.1.0' - compile 'com.netflix.feign:feign-gson:7.1.0' + compile 'com.netflix.feign:feign-core:7.2.1' + compile 'com.netflix.feign:feign-gson:7.2.1' } // create a self-contained jar that is executable diff --git a/example-github/pom.xml b/example-github/pom.xml index 778608ad71..bbbc3ff1d2 100644 --- a/example-github/pom.xml +++ b/example-github/pom.xml @@ -12,7 +12,7 @@ com.netflix.feign feign-example-github jar - 7.1.0 + 7.2.1 GitHub Example diff --git a/example-wikipedia/build.gradle b/example-wikipedia/build.gradle index e9489a488d..b8143270e9 100644 --- a/example-wikipedia/build.gradle +++ b/example-wikipedia/build.gradle @@ -12,8 +12,8 @@ configurations { } dependencies { - compile 'com.netflix.feign:feign-core:7.1.0' - compile 'com.netflix.feign:feign-gson:7.1.0' + compile 'com.netflix.feign:feign-core:7.2.1' + compile 'com.netflix.feign:feign-gson:7.2.1' } // create a self-contained jar that is executable diff --git a/example-wikipedia/pom.xml b/example-wikipedia/pom.xml index 144378ca4a..38127f853c 100644 --- a/example-wikipedia/pom.xml +++ b/example-wikipedia/pom.xml @@ -12,7 +12,7 @@ com.netflix.feign feign-example-wikipedia jar - 7.1.0 + 7.2.1 Wikipedia Example From 47356902892dc5d49ab0afacba2fa8214b679048 Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Sat, 7 Feb 2015 10:20:33 -0800 Subject: [PATCH 173/179] Updates to Ribbon 2.0-RC13 --- CHANGELOG.md | 4 ++++ ribbon/build.gradle | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5269a2ccba..8ab08053ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Version 7.3 * Adds Request.Options support to RibbonClient +* Updates to Ribbon 2.0-RC13 ### Version 7.2 * Adds `Feign.Builder.build()` @@ -26,6 +27,9 @@ * Upgrade to Dagger 1.2.2. * **Note:** Dagger-generated code prior to version 1.2.0 is incompatible with Dagger 1.2.0 and beyond. Dagger users should upgrade Dagger to at least version 1.2.0, and recompile any dependency-injected classes. +### Version 6.1.3 +* Updates to Ribbon 2.0-RC5 + ### Version 6.1.1 * Fix for #85 diff --git a/ribbon/build.gradle b/ribbon/build.gradle index 05b2c6b73f..ea7dbfd1e4 100644 --- a/ribbon/build.gradle +++ b/ribbon/build.gradle @@ -4,7 +4,7 @@ sourceCompatibility = 1.6 dependencies { compile project(':feign-core') - compile 'com.netflix.ribbon:ribbon-loadbalancer:2.0-RC5' + compile 'com.netflix.ribbon:ribbon-loadbalancer:2.0-RC13' testCompile 'junit:junit:4.12' testCompile 'org.assertj:assertj-core:1.7.1' testCompile 'com.squareup.okhttp:mockwebserver:2.2.0' From a5093937e5bccbeda7edd02ec59fa8630fc4a120 Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Sat, 7 Feb 2015 10:29:32 -0800 Subject: [PATCH 174/179] Updates to Jackson 2.5.1 --- CHANGELOG.md | 1 + jackson/build.gradle | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ab08053ec..c4a0a2a65e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ ### Version 7.3 * Adds Request.Options support to RibbonClient * Updates to Ribbon 2.0-RC13 +* Updates to Jackson 2.5.1 ### Version 7.2 * Adds `Feign.Builder.build()` diff --git a/jackson/build.gradle b/jackson/build.gradle index d8b7ea9f38..c1fca11b0f 100644 --- a/jackson/build.gradle +++ b/jackson/build.gradle @@ -4,7 +4,7 @@ sourceCompatibility = 1.6 dependencies { compile project(':feign-core') - compile 'com.fasterxml.jackson.core:jackson-databind:2.2.2' + compile 'com.fasterxml.jackson.core:jackson-databind:2.5.1' testCompile 'junit:junit:4.12' testCompile 'org.assertj:assertj-core:1.7.1' testCompile project(':feign-core').sourceSets.test.output // for assertions From 05b58948f5fe943421632675a0a4971f46ee70d0 Mon Sep 17 00:00:00 2001 From: Brendan Nolan Date: Thu, 12 Feb 2015 21:31:22 +0000 Subject: [PATCH 175/179] Retains scheme in LBClient.RibbonRequest URI Before this change, we were dropping scheme, which prevented use of https. closes #183 --- .../client/TrustingSSLSocketFactory.java | 2 +- ribbon/build.gradle | 1 + .../main/java/feign/ribbon/RibbonClient.java | 14 +++++-------- .../java/feign/ribbon/RibbonClientTest.java | 21 +++++++++++++++++++ 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/core/src/test/java/feign/client/TrustingSSLSocketFactory.java b/core/src/test/java/feign/client/TrustingSSLSocketFactory.java index aa15be208a..c3bec6afe9 100644 --- a/core/src/test/java/feign/client/TrustingSSLSocketFactory.java +++ b/core/src/test/java/feign/client/TrustingSSLSocketFactory.java @@ -40,7 +40,7 @@ /** * Used for ssl tests to simplify setup. */ -final class TrustingSSLSocketFactory extends SSLSocketFactory +public final class TrustingSSLSocketFactory extends SSLSocketFactory implements X509TrustManager, X509KeyManager { private static final Map diff --git a/ribbon/build.gradle b/ribbon/build.gradle index ea7dbfd1e4..713f8a50ef 100644 --- a/ribbon/build.gradle +++ b/ribbon/build.gradle @@ -8,4 +8,5 @@ dependencies { testCompile 'junit:junit:4.12' testCompile 'org.assertj:assertj-core:1.7.1' testCompile 'com.squareup.okhttp:mockwebserver:2.2.0' + testCompile project(':feign-core').sourceSets.test.output } diff --git a/ribbon/src/main/java/feign/ribbon/RibbonClient.java b/ribbon/src/main/java/feign/ribbon/RibbonClient.java index 263ac00ac3..864070ee5b 100644 --- a/ribbon/src/main/java/feign/ribbon/RibbonClient.java +++ b/ribbon/src/main/java/feign/ribbon/RibbonClient.java @@ -1,5 +1,8 @@ package feign.ribbon; +import java.io.IOException; +import java.net.URI; + import com.netflix.client.ClientException; import com.netflix.client.ClientFactory; import com.netflix.client.config.CommonClientConfigKey; @@ -7,9 +10,6 @@ import com.netflix.client.config.IClientConfig; import com.netflix.loadbalancer.ILoadBalancer; -import java.io.IOException; -import java.net.URI; - import feign.Client; import feign.Request; import feign.Response; @@ -41,12 +41,8 @@ public Response execute(Request request, Request.Options options) throws IOExcep try { URI asUri = URI.create(request.url()); String clientName = asUri.getHost(); - URI - uriWithoutSchemeAndPort = - URI.create(request.url().replace(asUri.getScheme() + "://" + asUri.getHost(), "")); - LBClient.RibbonRequest - ribbonRequest = - new LBClient.RibbonRequest(request, uriWithoutSchemeAndPort); + URI uriWithoutHost = URI.create(request.url().replace(asUri.getHost(), "")); + LBClient.RibbonRequest ribbonRequest = new LBClient.RibbonRequest(request, uriWithoutHost); return lbClient(clientName).executeWithLoadBalancer(ribbonRequest, new FeignOptionsClientConfig(options)).toResponse(); } catch (ClientException e) { diff --git a/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java b/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java index 7faed74394..3e6967d700 100644 --- a/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java +++ b/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java @@ -34,10 +34,12 @@ import com.squareup.okhttp.mockwebserver.SocketPolicy; import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule; +import feign.Client; import feign.Feign; import feign.Param; import feign.Request; import feign.RequestLine; +import feign.client.TrustingSSLSocketFactory; public class RibbonClientTest { @@ -123,6 +125,25 @@ public void urlEncodeQueryStringParameters() throws IOException, InterruptedExce assertEquals(recordedRequestLine, expectedRequestLine); } + + @Test + public void testHTTPSViaRibbon() { + + Client trustSSLSockets = new Client.Default(TrustingSSLSocketFactory.get(), null); + + server1.get().useHttps(TrustingSSLSocketFactory.get("localhost"), false); + server1.enqueue(new MockResponse().setBody("success!")); + + getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.getUrl(""))); + + TestInterface api = + Feign.builder().client(new RibbonClient(trustSSLSockets)) + .target(TestInterface.class, "https://" + client()); + api.post(); + assertEquals(1, server1.getRequestCount()); + + } + @Test public void ioExceptionRetryWithBuilder() throws IOException, InterruptedException { server1.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); From 45770824957e13766688a6af093f9073d2de5595 Mon Sep 17 00:00:00 2001 From: Jacques-Etienne Beaudet Date: Sat, 21 Feb 2015 17:50:30 +0000 Subject: [PATCH 176/179] Supports query params without values Fixes NPE when building a client with a query param with no values --- CHANGELOG.md | 1 + core/src/main/java/feign/RequestTemplate.java | 8 ++---- core/src/main/java/feign/Util.java | 2 +- .../test/java/feign/DefaultContractTest.java | 28 +++++++++++++++++-- 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4a0a2a65e..dbc79a256d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * Adds Request.Options support to RibbonClient * Updates to Ribbon 2.0-RC13 * Updates to Jackson 2.5.1 +* Supports query parameters without values ### Version 7.2 * Adds `Feign.Builder.build()` diff --git a/core/src/main/java/feign/RequestTemplate.java b/core/src/main/java/feign/RequestTemplate.java index 607a90b3b2..a7c7a69d2d 100644 --- a/core/src/main/java/feign/RequestTemplate.java +++ b/core/src/main/java/feign/RequestTemplate.java @@ -145,11 +145,7 @@ private static Map> parseAndDecodeQueries(String quer return map; } if (queryLine.indexOf('&') == -1) { - if (queryLine.indexOf('=') != -1) { - putKV(queryLine, map); - } else { - map.put(queryLine, null); - } + putKV(queryLine, map); } else { char[] chars = queryLine.toCharArray(); int start = 0; @@ -504,7 +500,7 @@ private StringBuilder pullAnyQueriesOutOfUrl(StringBuilder url) { } private boolean allValuesAreNull(Collection values) { - if (values.isEmpty()) { + if (values == null || values.isEmpty()) { return true; } for (String val : values) { diff --git a/core/src/main/java/feign/Util.java b/core/src/main/java/feign/Util.java index 7469c9b03f..3e044ddc0b 100644 --- a/core/src/main/java/feign/Util.java +++ b/core/src/main/java/feign/Util.java @@ -139,7 +139,7 @@ public static T[] toArray(Iterable iterable, Class type) { * Returns an unmodifiable collection which may be empty, but is never null. */ public static Collection valuesOrEmpty(Map> map, String key) { - return map.containsKey(key) ? map.get(key) : Collections.emptyList(); + return map.containsKey(key) && map.get(key) != null ? map.get(key) : Collections.emptyList(); } public static void ensureClosed(Closeable closeable) { diff --git a/core/src/test/java/feign/DefaultContractTest.java b/core/src/test/java/feign/DefaultContractTest.java index 99b1b7a727..2bfc4adc66 100644 --- a/core/src/test/java/feign/DefaultContractTest.java +++ b/core/src/test/java/feign/DefaultContractTest.java @@ -22,6 +22,7 @@ import org.junit.rules.ExpectedException; import java.net.URI; +import java.util.Collections; import java.util.Date; import java.util.List; @@ -124,7 +125,7 @@ public void queryParamsInPathExtract() throws Exception { ); assertThat( - contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("empty")) + contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("twoAndOneEmpty")) .template()) .hasUrl("/") .hasQueries( @@ -132,6 +133,23 @@ public void queryParamsInPathExtract() throws Exception { entry("Action", asList("GetUser")), entry("Version", asList("2010-05-08")) ); + + assertThat( + contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("oneEmpty")) + .template()) + .hasUrl("/") + .hasQueries( + entry("flag", asList(new String[]{null})) + ); + + assertThat( + contract.parseAndValidatateMetadata(WithQueryParamsInPath.class.getDeclaredMethod("twoEmpty")) + .template()) + .hasUrl("/") + .hasQueries( + entry("flag", asList(new String[]{null})), + entry("NoErrors", asList(new String[]{null})) + ); } @Test @@ -307,7 +325,13 @@ interface WithQueryParamsInPath { Response three(); @RequestLine("GET /?flag&Action=GetUser&Version=2010-05-08") - Response empty(); + Response twoAndOneEmpty(); + + @RequestLine("GET /?flag") + Response oneEmpty(); + + @RequestLine("GET /?flag&NoErrors") + Response twoEmpty(); } interface BodyWithoutParameters { From 222793057764d138399fff8fc857a1649ef8705a Mon Sep 17 00:00:00 2001 From: Brendan Nolan Date: Thu, 19 Feb 2015 20:54:45 +0000 Subject: [PATCH 177/179] Adds LBClientFactory to enable caching of Ribbon LBClients Before, LBClients were created for each request, which led to issues such as #182. Moreover, a user could not avoid using Ribbon's static factories. Adding LBClientFactory allows users to control how Ribbon resources are created. --- CHANGELOG.md | 1 + README.md | 2 +- .../src/main/java/feign/ribbon/LBClient.java | 24 ++++--- .../java/feign/ribbon/LBClientFactory.java | 22 ++++++ .../feign/ribbon/LoadBalancingTarget.java | 2 +- .../main/java/feign/ribbon/RibbonClient.java | 68 ++++++++++++++++--- .../feign/ribbon/LBClientFactoryTest.java | 18 +++++ .../java/feign/ribbon/RibbonClientTest.java | 14 ++-- 8 files changed, 123 insertions(+), 28 deletions(-) create mode 100644 ribbon/src/main/java/feign/ribbon/LBClientFactory.java create mode 100644 ribbon/src/test/java/feign/ribbon/LBClientFactoryTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index dbc79a256d..4cc5cfe5c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Version 7.3 * Adds Request.Options support to RibbonClient +* Adds LBClientFactory to enable caching of Ribbon LBClients * Updates to Ribbon 2.0-RC13 * Updates to Jackson 2.5.1 * Supports query parameters without values diff --git a/README.md b/README.md index 478962fd27..87b768bf04 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,7 @@ GitHub github = Feign.builder() Integration requires you to pass your ribbon client name as the host part of the url, for example `myAppProd`. ```java -MyService api = Feign.builder().client(new RibbonClient()).target(MyService.class, "https://myAppProd"); +MyService api = Feign.builder().client(RibbonClient.create()).target(MyService.class, "https://myAppProd"); ``` diff --git a/ribbon/src/main/java/feign/ribbon/LBClient.java b/ribbon/src/main/java/feign/ribbon/LBClient.java index c3a4ca0d3a..0d5a7b9886 100644 --- a/ribbon/src/main/java/feign/ribbon/LBClient.java +++ b/ribbon/src/main/java/feign/ribbon/LBClient.java @@ -35,19 +35,21 @@ import feign.RequestTemplate; import feign.Response; -class LBClient - extends AbstractLoadBalancerAwareClient { +public final class LBClient extends + AbstractLoadBalancerAwareClient { - private final Client delegate; private final int connectTimeout; private final int readTimeout; private final IClientConfig clientConfig; - LBClient(Client delegate, ILoadBalancer lb, IClientConfig clientConfig) { + public static LBClient create(ILoadBalancer lb, IClientConfig clientConfig) { + return new LBClient(lb, clientConfig); + } + + LBClient(ILoadBalancer lb, IClientConfig clientConfig) { super(lb, clientConfig); this.setRetryHandler(RetryHandler.DEFAULT); this.clientConfig = clientConfig; - this.delegate = delegate; connectTimeout = clientConfig.get(CommonClientConfigKey.ConnectTimeout); readTimeout = clientConfig.get(CommonClientConfigKey.ReadTimeout); } @@ -64,7 +66,7 @@ public RibbonResponse execute(RibbonRequest request, IClientConfig configOverrid } else { options = new Request.Options(connectTimeout, readTimeout); } - Response response = delegate.execute(request.toRequest(), options); + Response response = request.client().execute(request.toRequest(), options); return new RibbonResponse(request.getUri(), response); } @@ -84,8 +86,10 @@ public RequestSpecificRetryHandler getRequestSpecificRetryHandler( static class RibbonRequest extends ClientRequest implements Cloneable { private final Request request; + private final Client client; - RibbonRequest(Request request, URI uri) { + RibbonRequest(Client client, Request request, URI uri) { + this.client = client; this.request = request; setUri(uri); } @@ -99,8 +103,12 @@ Request toRequest() { .request(); } + Client client() { + return client; + } + public Object clone() { - return new RibbonRequest(request, getUri()); + return new RibbonRequest(client, request, getUri()); } } diff --git a/ribbon/src/main/java/feign/ribbon/LBClientFactory.java b/ribbon/src/main/java/feign/ribbon/LBClientFactory.java new file mode 100644 index 0000000000..30bd8c98b6 --- /dev/null +++ b/ribbon/src/main/java/feign/ribbon/LBClientFactory.java @@ -0,0 +1,22 @@ +package feign.ribbon; + +import com.netflix.client.ClientFactory; +import com.netflix.client.config.IClientConfig; +import com.netflix.loadbalancer.ILoadBalancer; + +public interface LBClientFactory { + + LBClient create(String clientName); + + /** + * Uses {@link ClientFactory} static factories from ribbon to create an LBClient. + */ + public static final class Default implements LBClientFactory { + @Override + public LBClient create(String clientName) { + IClientConfig config = ClientFactory.getNamedConfig(clientName); + ILoadBalancer lb = ClientFactory.getNamedLoadBalancer(clientName); + return LBClient.create(lb, config); + } + } +} diff --git a/ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java b/ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java index 95162a6a77..81c3f7cc1a 100644 --- a/ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java +++ b/ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java @@ -104,7 +104,7 @@ public Request apply(RequestTemplate input) { @Override public boolean equals(Object obj) { if (obj instanceof LoadBalancingTarget) { - LoadBalancingTarget other = (LoadBalancingTarget) obj; + LoadBalancingTarget other = (LoadBalancingTarget) obj; return type.equals(other.type) && name.equals(other.name); } diff --git a/ribbon/src/main/java/feign/ribbon/RibbonClient.java b/ribbon/src/main/java/feign/ribbon/RibbonClient.java index 864070ee5b..18ecaa90c3 100644 --- a/ribbon/src/main/java/feign/ribbon/RibbonClient.java +++ b/ribbon/src/main/java/feign/ribbon/RibbonClient.java @@ -4,36 +4,58 @@ import java.net.URI; import com.netflix.client.ClientException; -import com.netflix.client.ClientFactory; import com.netflix.client.config.CommonClientConfigKey; import com.netflix.client.config.DefaultClientConfigImpl; -import com.netflix.client.config.IClientConfig; -import com.netflix.loadbalancer.ILoadBalancer; import feign.Client; import feign.Request; import feign.Response; /** - * RibbonClient can be used in Fiegn builder to activate smart routing and resiliency capabilities + * RibbonClient can be used in Feign builder to activate smart routing and resiliency capabilities * provided by Ribbon. Ex. + * *
- * MyService api = Feign.builder.client(new RibbonClient()).target(MyService.class,
- * "http://myAppProd");
+ * MyService api = Feign.builder.client(RibbonClient.create()).target(MyService.class,
+ *     "http://myAppProd");
  * 
+ * * Where {@code myAppProd} is the ribbon client name and {@code myAppProd.ribbon.listOfServers} * configuration is set. */ public class RibbonClient implements Client { private final Client delegate; + private final LBClientFactory lbClientFactory; + + public static RibbonClient create() { + return builder().build(); + } + + public static Builder builder() { + return new Builder(); + } + + /** + * @deprecated Use the {@link RibbonClient#create()} + */ + @Deprecated public RibbonClient() { - this.delegate = new Client.Default(null, null); + this(new Client.Default(null, null)); } + /** + * @deprecated Use the {@link RibbonClient#create()} + */ + @Deprecated public RibbonClient(Client delegate) { + this(delegate, new LBClientFactory.Default()); + } + + RibbonClient(Client delegate, LBClientFactory lbClientFactory) { this.delegate = delegate; + this.lbClientFactory = lbClientFactory; } @Override @@ -42,7 +64,8 @@ public Response execute(Request request, Request.Options options) throws IOExcep URI asUri = URI.create(request.url()); String clientName = asUri.getHost(); URI uriWithoutHost = URI.create(request.url().replace(asUri.getHost(), "")); - LBClient.RibbonRequest ribbonRequest = new LBClient.RibbonRequest(request, uriWithoutHost); + LBClient.RibbonRequest ribbonRequest = + new LBClient.RibbonRequest(delegate, request, uriWithoutHost); return lbClient(clientName).executeWithLoadBalancer(ribbonRequest, new FeignOptionsClientConfig(options)).toResponse(); } catch (ClientException e) { @@ -54,9 +77,7 @@ public Response execute(Request request, Request.Options options) throws IOExcep } private LBClient lbClient(String clientName) { - IClientConfig config = ClientFactory.getNamedConfig(clientName); - ILoadBalancer lb = ClientFactory.getNamedLoadBalancer(clientName); - return new LBClient(delegate, lb, config); + return lbClientFactory.create(clientName); } static class FeignOptionsClientConfig extends DefaultClientConfigImpl { @@ -78,4 +99,29 @@ public void loadDefaultValues() { } + public static final class Builder { + + Builder() { + } + + private Client delegate; + private LBClientFactory lbClientFactory; + + public Builder delegate(Client delegate) { + this.delegate = delegate; + return this; + } + + public Builder lbClientFactory(LBClientFactory lbClientFactory) { + this.lbClientFactory = lbClientFactory; + return this; + } + + public RibbonClient build() { + return new RibbonClient( + delegate != null ? delegate : new Client.Default(null, null), + lbClientFactory != null ? lbClientFactory : new LBClientFactory.Default() + ); + } + } } diff --git a/ribbon/src/test/java/feign/ribbon/LBClientFactoryTest.java b/ribbon/src/test/java/feign/ribbon/LBClientFactoryTest.java new file mode 100644 index 0000000000..3eccf50a2d --- /dev/null +++ b/ribbon/src/test/java/feign/ribbon/LBClientFactoryTest.java @@ -0,0 +1,18 @@ +package feign.ribbon; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import com.netflix.client.ClientFactory; + +public class LBClientFactoryTest { + + @Test + public void testCreateLBClient() { + LBClientFactory.Default lbClientFactory = new LBClientFactory.Default(); + LBClient client = lbClientFactory.create("clientName"); + assertEquals("clientName", client.getClientName()); + assertEquals(ClientFactory.getNamedLoadBalancer("clientName"), client.getLoadBalancer()); + } +} diff --git a/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java b/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java index 3e6967d700..82ca857800 100644 --- a/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java +++ b/ribbon/src/test/java/feign/ribbon/RibbonClientTest.java @@ -66,7 +66,7 @@ public void loadBalancingDefaultPolicyRoundRobin() throws IOException, Interrupt TestInterface api = - Feign.builder().client(new RibbonClient()) + Feign.builder().client(RibbonClient.create()) .target(TestInterface.class, "http://" + client()); api.post(); @@ -87,7 +87,7 @@ public void ioExceptionRetry() throws IOException, InterruptedException { TestInterface api = - Feign.builder().client(new RibbonClient()) + Feign.builder().client(RibbonClient.create()) .target(TestInterface.class, "http://" + client()); api.post(); @@ -115,7 +115,7 @@ public void urlEncodeQueryStringParameters() throws IOException, InterruptedExce TestInterface api = - Feign.builder().client(new RibbonClient()) + Feign.builder().client(RibbonClient.create()) .target(TestInterface.class, "http://" + client()); api.getWithQueryParameters(queryStringValue); @@ -137,7 +137,7 @@ public void testHTTPSViaRibbon() { getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.getUrl(""))); TestInterface api = - Feign.builder().client(new RibbonClient(trustSSLSockets)) + Feign.builder().client(RibbonClient.builder().delegate(trustSSLSockets).build()) .target(TestInterface.class, "https://" + client()); api.post(); assertEquals(1, server1.getRequestCount()); @@ -151,9 +151,9 @@ public void ioExceptionRetryWithBuilder() throws IOException, InterruptedExcepti getConfigInstance().setProperty(serverListKey(), hostAndPort(server1.getUrl(""))); - TestInterface api = Feign.builder(). - client(new RibbonClient()). - target(TestInterface.class, "http://" + client()); + TestInterface api = + Feign.builder().client(RibbonClient.create()) + .target(TestInterface.class, "http://" + client()); api.post(); From ed0955d34a09df386270507ed2e1730552bb681c Mon Sep 17 00:00:00 2001 From: Stefan Fussenegger Date: Wed, 25 Feb 2015 09:51:24 +0100 Subject: [PATCH 178/179] adds ErrorDecoder example with nested Decoder --- .../feign/example/github/GitHubExample.java | 60 ++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/example-github/src/main/java/feign/example/github/GitHubExample.java b/example-github/src/main/java/feign/example/github/GitHubExample.java index cca535e90d..cc7d79c856 100644 --- a/example-github/src/main/java/feign/example/github/GitHubExample.java +++ b/example-github/src/main/java/feign/example/github/GitHubExample.java @@ -21,6 +21,9 @@ import feign.Logger; import feign.Param; import feign.RequestLine; +import feign.Response; +import feign.codec.Decoder; +import feign.codec.ErrorDecoder; import feign.gson.GsonDecoder; /** @@ -29,8 +32,10 @@ public class GitHubExample { public static void main(String... args) throws InterruptedException { + Decoder decoder = new GsonDecoder(); GitHub github = Feign.builder() - .decoder(new GsonDecoder()) + .decoder(decoder) + .errorDecoder(new GitHubErrorDecoder(decoder)) .logger(new Logger.ErrorLogger()) .logLevel(Logger.Level.BASIC) .target(GitHub.class, "https://api.github.com"); @@ -40,6 +45,12 @@ public static void main(String... args) throws InterruptedException { for (Contributor contributor : contributors) { System.out.println(contributor.login + " (" + contributor.contributions + ")"); } + + try { + contributors = github.contributors("netflix", "some-unknown-project"); + } catch (GitHubClientError e) { + System.out.println(e.error.message); + } } interface GitHub { @@ -53,4 +64,51 @@ static class Contributor { String login; int contributions; } + + static class ClientError { + + String message; + List errors; + } + + static class Error { + String resource; + String field; + String code; + } + + static class GitHubErrorDecoder implements ErrorDecoder { + + final Decoder decoder; + final ErrorDecoder defaultDecoder = new ErrorDecoder.Default(); + + public GitHubErrorDecoder(Decoder decoder) { + this.decoder = decoder; + } + + public Exception decode(String methodKey, Response response) { + if (response.status() >= 400 && response.status() < 500) { + try { + ClientError error = (ClientError) decoder.decode(response, ClientError.class ); + return new GitHubClientError(response.status(), error); + } catch (Exception e) { + e.printStackTrace(); + } + } + return defaultDecoder.decode(methodKey, response); + } + } + + static class GitHubClientError extends RuntimeException { + + private static final long serialVersionUID = 0; + + ClientError error; + + protected GitHubClientError(int status, ClientError error) { + super("client error " + status); + this.error = error; + } + + } } From b8c2c0ea3f5b18f67a3afa18a6a44881b36e1357 Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Wed, 25 Feb 2015 07:55:13 -0800 Subject: [PATCH 179/179] Polishes GitHub example The GitHub example could be better organized as top-down. Also, it is easier to show basic error decoding when there is less structure. --- .../feign/example/github/GitHubExample.java | 81 +++++++------------ 1 file changed, 31 insertions(+), 50 deletions(-) diff --git a/example-github/src/main/java/feign/example/github/GitHubExample.java b/example-github/src/main/java/feign/example/github/GitHubExample.java index cc7d79c856..26058c8048 100644 --- a/example-github/src/main/java/feign/example/github/GitHubExample.java +++ b/example-github/src/main/java/feign/example/github/GitHubExample.java @@ -15,6 +15,7 @@ */ package feign.example.github; +import java.io.IOException; import java.util.List; import feign.Feign; @@ -27,11 +28,30 @@ import feign.gson.GsonDecoder; /** - * adapted from {@code com.example.retrofit.GitHubClient} + * Inspired by {@code com.example.retrofit.GitHubClient} */ public class GitHubExample { - public static void main(String... args) throws InterruptedException { + interface GitHub { + @RequestLine("GET /repos/{owner}/{repo}/contributors") + List contributors(@Param("owner") String owner, @Param("repo") String repo); + } + + static class Contributor { + String login; + int contributions; + } + + static class GitHubClientError extends RuntimeException { + private String message; // parsed from json + + @Override + public String getMessage() { + return message; + } + } + + public static void main(String... args) { Decoder decoder = new GsonDecoder(); GitHub github = Feign.builder() .decoder(decoder) @@ -46,69 +66,30 @@ public static void main(String... args) throws InterruptedException { System.out.println(contributor.login + " (" + contributor.contributions + ")"); } + System.out.println("Now, let's cause an error."); try { - contributors = github.contributors("netflix", "some-unknown-project"); + github.contributors("netflix", "some-unknown-project"); } catch (GitHubClientError e) { - System.out.println(e.error.message); + System.out.println(e.getMessage()); } } - interface GitHub { - - @RequestLine("GET /repos/{owner}/{repo}/contributors") - List contributors(@Param("owner") String owner, @Param("repo") String repo); - } - - static class Contributor { - - String login; - int contributions; - } - - static class ClientError { - - String message; - List errors; - } - - static class Error { - String resource; - String field; - String code; - } - static class GitHubErrorDecoder implements ErrorDecoder { final Decoder decoder; final ErrorDecoder defaultDecoder = new ErrorDecoder.Default(); - public GitHubErrorDecoder(Decoder decoder) { + GitHubErrorDecoder(Decoder decoder) { this.decoder = decoder; } + @Override public Exception decode(String methodKey, Response response) { - if (response.status() >= 400 && response.status() < 500) { - try { - ClientError error = (ClientError) decoder.decode(response, ClientError.class ); - return new GitHubClientError(response.status(), error); - } catch (Exception e) { - e.printStackTrace(); - } + try { + return (Exception) decoder.decode(response, GitHubClientError.class); + } catch (IOException fallbackToDefault) { + return defaultDecoder.decode(methodKey, response); } - return defaultDecoder.decode(methodKey, response); - } - } - - static class GitHubClientError extends RuntimeException { - - private static final long serialVersionUID = 0; - - ClientError error; - - protected GitHubClientError(int status, ClientError error) { - super("client error " + status); - this.error = error; } - } }

+ * Configuration keys are formatted as unresolved see tags. + *