From 697fd66aae9beed107e13f49a741455f1d9d8dd9 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Sun, 18 Mar 2012 01:07:52 -0700 Subject: [PATCH 001/125] 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 000000000..b1ae8bae1 --- /dev/null +++ b/.classpath @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..618e741f8 --- /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 000000000..f2d845e45 --- /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 000000000..e86c91081 --- /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 000000000..445ff6da6 --- /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 000000000..01e59693e --- /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 000000000..5297034a5 --- /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 000000000..3c8a8e6c7 --- /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 000000000..cf6f0461a --- /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 000000000..a3fc06dd0 --- /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 000000000..8639564ce --- /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 000000000..a87bc54ef --- /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 000000000..8a0b282aa --- /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 000000000..350f2f1b4 --- /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 000000000..cd1d475d6 --- /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 000000000..66279e9c1 --- /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 000000000..1e7f02283 --- /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 000000000..394fb107f --- /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 000000000..c3ebf8609 --- /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 000000000..b85e23e98 --- /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 000000000..bf561a6d5 --- /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 000000000..09505c5c7 --- /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 000000000..0b2a3869f --- /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 000000000..1e7f02283 --- /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 000000000..394fb107f --- /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 000000000..a856ce88a --- /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 000000000..273135a29 --- /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/125] 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 b1ae8bae1..000000000 --- a/.classpath +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/.gitignore b/.gitignore index 618e741f8..313af3cb8 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 f2d845e45..000000000 --- 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 e86c91081..000000000 --- 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 445ff6da6..000000000 --- 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 01e59693e..000000000 --- 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 000000000..7f8ced0d1 --- /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 5297034a5..9eef3329e 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 000000000..b27b19292 --- /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 3c8a8e6c7..481d2829f 100644 --- a/codequality/checkstyle.xml +++ b/codequality/checkstyle.xml @@ -50,6 +50,7 @@ + diff --git a/gradle/check.gradle b/gradle/check.gradle index cf6f0461a..0f80516d4 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 000000000..9d0483032 --- /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 000000000..6f2d204b8 --- /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 8639564ce..cb75dfb63 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 c3ebf8609..fc9d20d33 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 b85e23e98..c190f03bb 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 bf561a6d5..616f72efb 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/125] 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 9eef3329e..c0d2d5e7f 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 a3fc06dd0..925516120 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 9d0483032..1fdc2702b 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 cb75dfb63..ab2792ff3 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 000000000..8fc34dbff --- /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/125] 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 c0d2d5e7f..0fc71a505 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 ab2792ff3..3de099046 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/125] 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 6f2d204b8..000000000 --- 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/125] 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 3de099046..1673a24f8 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/125] Fix quotes --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0fc71a505..55e5eeb55 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/125] 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 1673a24f8..560e66b4d 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/125] 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 cd1d475d6..000000000 --- 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 66279e9c1..000000000 --- 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 1e7f02283..000000000 --- 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 394fb107f..000000000 --- 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 fc9d20d33..000000000 --- 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 c190f03bb..000000000 --- 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 616f72efb..000000000 --- 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 09505c5c7..000000000 --- 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 0b2a3869f..000000000 --- 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 1e7f02283..000000000 --- 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 394fb107f..000000000 --- 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 a856ce88a..000000000 --- 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 273135a29..000000000 --- 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/125] Un-indenting HEADER --- codequality/HEADER | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/codequality/HEADER b/codequality/HEADER index b27b19292..6c5c7c9c7 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/125] 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 55e5eeb55..db7039635 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/125] 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 560e66b4d..7efb83333 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/125] 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 db7039635..fae039b42 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 6c5c7c9c7..3102e4b44 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 000000000..77d13d102 --- /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 1fdc2702b..11a51f113 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/125] 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 925516120..919e38290 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 3798e7cf1..9829a99a5 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 ae91ed902..e61422d06 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 8a0b282aa..aec99730b 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/125] 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 77d13d102..328acfc31 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 0f80516d4..7617f17b3 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/125] 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 fae039b42..65b498ec8 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 000000000..6b59bf6c4 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +version=1.4-SNAPSHOT diff --git a/gradle/buildscript.gradle b/gradle/buildscript.gradle index 328acfc31..59ffb3d33 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 919e38290..65da4e30d 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 7efb83333..29f5d405d 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 8fc34dbff..fe4bc2ebd 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/125] 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 59ffb3d33..d12c78383 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/125] 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 d12c78383..c63c13006 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 fe4bc2ebd..8ed030510 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/125] Setting default name for multi-project --- settings.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/settings.gradle b/settings.gradle index 350f2f1b4..5dd25eb8c 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/125] 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 313af3cb8..79ab4710f 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 65da4e30d..ce2701a8c 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 29f5d405d..581896b15 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 8ed030510..a7f9913d0 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/125] 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 65b498ec8..7f8fb72de 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 581896b15..1b1e818fd 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/125] 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 1b1e818fd..a3a3d4424 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/125] Add local publishing --- .gitignore | 1 + gradle/release.gradle | 26 ++++++++++++-------------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 79ab4710f..c82b5347c 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 a7f9913d0..cd135643b 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/125] 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 ce2701a8c..f16047e2f 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/125] 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 f16047e2f..8e71812ec 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/125] 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 c63c13006..4d6a29aab 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/125] 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 8e71812ec..6122c8e87 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/125] 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 6122c8e87..8b877071d 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/125] 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 481d2829f..47c01a2ea 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/125] 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 8b877071d..4f07d1a64 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 9829a99a5..da2f8ac95 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 e61422d06..91a7e269e 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/125] 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 7f8fb72de..80e2f17fb 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/125] 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 4d6a29aab..d3f06ec89 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/125] 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 80e2f17fb..582aa14d1 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 d3f06ec89..2cb8e60a1 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 11a51f113..abd2e2c0e 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/125] Add sonatype snapshot repository --- gradle/maven.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gradle/maven.gradle b/gradle/maven.gradle index a3a3d4424..3bf788d3e 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/125] 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 cd135643b..669c1db68 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/125] 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 582aa14d1..5e6c63bb9 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 2cb8e60a1..b6fb61e16 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 7617f17b3..a3e4b4e7f 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 4f07d1a64..36650a2f9 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 3bf788d3e..b92475762 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 669c1db68..23a1a6822 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/125] 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 da2f8ac95..061b536b4 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/125] 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 b6fb61e16..0f6555176 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 36650a2f9..1056bd5de 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/125] 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 23a1a6822..9daa84736 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/125] 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 b92475762..817846d77 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/125] 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 0f6555176..0b6da7ce8 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 1056bd5de..2720c8b40 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 9daa84736..06acd17da 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/125] 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 06acd17da..7979dc3a1 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/125] Fixing aggregateJavadoc --- gradle/convention.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gradle/convention.gradle b/gradle/convention.gradle index 2720c8b40..c4658fc33 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/125] 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 c82b5347c..5b07c032e 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 ebf660a86..0bf18dc73 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 5e6c63bb9..f99c728e5 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 000000000..5659f283c --- /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 000000000..ab30935db --- /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 000000000..d7a13cda6 --- /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 000000000..2375c5fa9 --- /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 000000000..a3a84b3d1 --- /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 000000000..409c5e624 --- /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 000000000..c65b885ac --- /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 000000000..30a47d966 --- /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 000000000..04922447f --- /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 000000000..6b68681a4 --- /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 000000000..8f1bcc749 --- /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 000000000..697f06c1d --- /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 000000000..bd2724cb2 --- /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 000000000..d9cb08e28 --- /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 000000000..9f0b581de --- /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 000000000..f35c2e4d7 --- /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 000000000..12473e622 --- /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 000000000..7651d3ec5 --- /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 000000000..a03d68e78 --- /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 000000000..92b861b8a --- /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 000000000..087143577 --- /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 000000000..e3c96137e --- /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 000000000..337125d88 --- /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 000000000..7f2df044a --- /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 000000000..881fb0885 --- /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 000000000..29ae66562 --- /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 000000000..502764560 --- /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 000000000..a9be5b2df --- /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 000000000..76d813e68 --- /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 000000000..c63faf2ca --- /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 6b59bf6c4..8d0c7be96 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 5dd25eb8c..a98b5acfa 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/125] bump --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 8d0c7be96..07ff68b98 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/125] 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 f99c728e5..2325ad830 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 c65b885ac..a13e0c520 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 d9cb08e28..76e17b929 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 12473e622..54a797375 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 92b861b8a..1a0184c76 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 087143577..1c4a7948c 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 e3c96137e..9e2b1ed0e 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 7f2df044a..5db23bfe0 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 29ae66562..89e8d17cf 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 76d813e68..20af6532d 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/125] 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 0bf18dc73..8a6c81865 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 5659f283c..0fbee091e 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 ab30935db..f1db269c0 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 d7a13cda6..9b8bd3252 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 2375c5fa9..500c0af28 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 a3a84b3d1..e734dc11f 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 409c5e624..e9b02700a 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 a13e0c520..5936cd562 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 30a47d966..eb062c8c2 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 04922447f..b2b1d9adb 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 6b68681a4..2fa628c23 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 8f1bcc749..3b9d3065e 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 697f06c1d..832f41377 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 bd2724cb2..16f2aba44 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 76e17b929..f63b517e1 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 9f0b581de..5ee03d5e6 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 f35c2e4d7..3dbb910a1 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 54a797375..e8140b2d6 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 7651d3ec5..0ffd9cfbb 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 a03d68e78..08e77a43b 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 1a0184c76..5a36b2a13 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 1c4a7948c..72413d66e 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 9e2b1ed0e..faec7ff52 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 337125d88..57cd1b734 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 5db23bfe0..7c8daaf43 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 881fb0885..c28e35d45 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 89e8d17cf..fc08cc13b 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 502764560..90734c52c 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 a9be5b2df..1f4e473df 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 20af6532d..26cfc7425 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 c63faf2ca..0d3b6eeb3 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/125] 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 000000000..0541be172
--- /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 8a6c81865..a370808e7 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 000000000..337793ff2
--- /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 000000000..aadc0d68e --- /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 000000000..9bf32a97e --- /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 000000000..4e073b0ee --- /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 a98b5acfa..df86e405e 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/125] 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 9bf32a97e..aab95c05c 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 4e073b0ee..4fdd6ff7c 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/125] 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 0541be172..8456fa5db 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 832f41377..a18cd421e 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 57cd1b734..c36cca6dc 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/125] 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 8456fa5db..396970cd8 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 000000000..893ddf9e0 --- /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 000000000..3b7657c4f --- /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 df86e405e..d2dc7844e 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/125] 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 893ddf9e0..a9c44cab3 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/125] 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 a370808e7..bae3ef40f 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 e811d22f1..d12dd2cd4 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 0fbee091e..8a7b08cf3 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 f1db269c0..031fa302d 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 9b8bd3252..fe27c83b5 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 500c0af28..bb4c6e61e 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 e734dc11f..0761b927e 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 e9b02700a..2b8bc4d4b 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 5936cd562..2bd0beea7 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 eb062c8c2..3df1613c4 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 b2b1d9adb..9dfe51e23 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 2fa628c23..238001065 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 3b9d3065e..f5d6eabb9 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 a18cd421e..cad03c287 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 16f2aba44..70c5a7f36 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 000000000..c001aed24 --- /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 f63b517e1..2c054476b 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 5ee03d5e6..6631d3e6a 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 3dbb910a1..9eac156da 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 e8140b2d6..56e85981b 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 0ffd9cfbb..e19c5f005 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 5a36b2a13..36a84171e 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 72413d66e..b1ca2ab55 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 faec7ff52..164d7b78c 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 c36cca6dc..6ccc9c685 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 7c8daaf43..c9fdf89dc 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 c28e35d45..f13eeda7c 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 90734c52c..835b50c7a 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 1f4e473df..7f4e4fbac 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 26cfc7425..5da310df9 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 0d3b6eeb3..eacb1fc3f 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 923d20d17..134c289bf 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 337793ff2..87287915e 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 aab95c05c..1b041ae8d 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 4fdd6ff7c..9f79a1617 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/125] 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 396970cd8..1f5d7de19 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 bae3ef40f..e63c3c041 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 d12dd2cd4..56d1cd431 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 000000000..0104acfbf --- /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 031fa302d..407bedce3 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 fe27c83b5..ea8b8b362 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 000000000..ad96930b0 --- /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 2bd0beea7..c9c623932 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 000000000..9d19862c6 --- /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 9dfe51e23..5f085bee3 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 08e77a43b..3d3ab1e82 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 000000000..3642bb060
--- /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 c9fdf89dc..306de900f 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 f13eeda7c..173ce535e 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 000000000..dedc5a63b
--- /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 5da310df9..93d710804 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 eacb1fc3f..fccafbfee 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 000000000..07d6b9399
--- /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 164d7b78c..8aaa24ed3 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 000000000..3499a8515
--- /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 000000000..c46c6420d
--- /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 87287915e..e7bcadc42 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 1b041ae8d..29e834e0d 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 9f79a1617..2f49d5695 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 d2dc7844e..dc5b04fff 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/125] 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 0104acfbf..f4d5d2bdc 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 8a7b08cf3..2b7a1afef 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 ea8b8b362..7cfa35864 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 ad96930b0..b1d7061fe 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 3df1613c4..b40c956a0 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 9d19862c6..b344144c5 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 5f085bee3..5e49c45e4 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 238001065..6baa2b842 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 cad03c287..b6cafe5db 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 70c5a7f36..d489a10cf 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 6631d3e6a..74f7c026e 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 9eac156da..18244a479 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 56e85981b..39a1ac98b 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 e19c5f005..31d163ebf 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 3d3ab1e82..381f80c2d 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 3642bb060..672b1f877 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 8aaa24ed3..40a4370c8 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 e7bcadc42..c10aa5e6b 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 aadc0d68e..9054d1013 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/125] 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 07ff68b98..cd92d6b08 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/125] 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 a9c44cab3..1a5882372 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 3b7657c4f..48597d7e5 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/125] 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 0761b927e..149addf91 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 c001aed24..57206e8da 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 31d163ebf..08a75faaf 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 672b1f877..8fc0dc2b7 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 dedc5a63b..40c83eda8 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 07d6b9399..ae01579bc 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 40a4370c8..6f02f9f9f 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/125] 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 1f5d7de19..6d663579e 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 149addf91..7f6d9327d 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 08a75faaf..169935042 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 306de900f..fd060f8a0 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 835b50c7a..bd3b17835 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/125] 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 7f6d9327d..a643179e2 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 57206e8da..2d55d7e8b 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 2c054476b..fda8fae75 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 18244a479..5f0b243fb 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 b1ca2ab55..b9b43774d 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/125] 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 a643179e2..9d71170d8 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/125] 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 6d663579e..879d0cd7b 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 2b7a1afef..315ccd83e 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 bb4c6e61e..a7607924e 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 c9c623932..bc77f5e8b 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 2d55d7e8b..edd6513d0 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 5f0b243fb..c21ecbd18 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 39a1ac98b..d26831548 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 b9b43774d..2ad10a2ad 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 fd060f8a0..8adb8b67d 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/125] 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 879d0cd7b..4dd52ff54 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 a7607924e..7ceef0d13 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 9d71170d8..949bb4e7b 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 36a84171e..25cf8efc9 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 2ad10a2ad..3e34fc2b5 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 8adb8b67d..bfb4eadeb 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 93d710804..61b317e7d 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 3499a8515..f8692bf54 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/125] 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 ae01579bc..9e766387a 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/125] 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 4dd52ff54..685f9893a 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 e63c3c041..ea1622eba 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 7cfa35864..867d24f79 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 000000000..96fdea456 --- /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 949bb4e7b..f58bf3a5d 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 fda8fae75..000000000 --- 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 000000000..22ccc041b --- /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/125] 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 685f9893a..bf2eb4e20 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 ea1622eba..b60ba16e4 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 407bedce3..7669fb954 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 867d24f79..b5de31cf6 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 7ceef0d13..ebdf7b065 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 f58bf3a5d..0cef816e9 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 bc77f5e8b..50289e16e 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 5e49c45e4..5b2b9a46e 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 000000000..bfdc00fd5 --- /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 edd6513d0..eceb6139a 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 74f7c026e..000000000 --- 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 000000000..5efab25ba --- /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 98cefdaa9..afcc6406e 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 22ca4f7dc..80b61ce99 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 000000000..12d06ba34 --- /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 000000000..80614b53f --- /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 169935042..d9982360e 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 381f80c2d..000000000 --- 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 25cf8efc9..972fee9cc 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 3e34fc2b5..8711b2d42 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 8fc0dc2b7..dc6633007 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 bfb4eadeb..3155e1549 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 22ccc041b..d72d89cfa 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 000000000..63a2e9ae2 --- /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 bd3b17835..efab2c9a7 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 61b317e7d..502f0f3e2 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 fccafbfee..7f384e287 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 6f02f9f9f..64621840d 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 f8692bf54..722352ea5 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 c46c6420d..f30377506 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/125] 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 96fdea456..c9bec2aa5 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/125] 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 7669fb954..f9b6d7d29 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 000000000..90173be56 --- /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 0cef816e9..d686f1798 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 50289e16e..f6a37eb26 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 dc6633007..958a7785f 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 9e766387a..e9e2a5dba 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 64621840d..36888cad8 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/125] 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 b5de31cf6..171116f4d 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 d686f1798..42076ff5a 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 f6a37eb26..d4e227dc7 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 afcc6406e..8492d143b 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 d9982360e..273202d40 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 000000000..30f27a04b --- /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 000000000..3e9dc8e00
--- /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 3155e1549..ac91708ba 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/125] 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 bf2eb4e20..8c6102d97 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 b60ba16e4..95195ce31 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 502f0f3e2..5ecc8cb1d 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/125] 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 8c6102d97..30ab975ce 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 95195ce31..29ff2e2aa 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 56d1cd431..829eb46df 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 000000000..206990e74
--- /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 000000000..53cc8ac0d
--- /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 000000000..9dd61a982
--- /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 dc5b04fff..f15a2c397 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/125] 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 cd92d6b08..5594a271c 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/125] 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 1a5882372..55b0af2de 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 48597d7e5..3106e5116 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/125] 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 30ab975ce..4fb4714af 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 29ff2e2aa..c3349f60c 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 829eb46df..b47bdfa42 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 f9b6d7d29..eed9b7bd1 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 171116f4d..d5d103ea5 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 42076ff5a..104b338d2 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 5463af09b..14ca1f1a3 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 000000000..0ea6112e8
--- /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 90173be56..d0aa6c78c 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 d4e227dc7..37eebc7dc 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 000000000..1b327f747 --- /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 30f27a04b..00e11b4b2 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 3e9dc8e00..a3fa77bae 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 958a7785f..aaaaf7ebc 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 ac91708ba..a114c5473 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 5ecc8cb1d..080fd1893 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 53cc8ac0d..63873e53a 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 9dd61a982..6f7ddb551 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 e9e2a5dba..d22451696 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 36888cad8..0612e1606 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 722352ea5..80289f112 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/125] 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 4fb4714af..7f537e9c0 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 c9bec2aa5..48853b1f2 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 104b338d2..d7cbffff1 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 d72d89cfa..edd1c1688 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/125] 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 5594a271c..11d840377 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/125] 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 55b0af2de..ac174601d 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 3106e5116..4120576a0 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/125] 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 ac174601d..94da11504 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 4120576a0..81e9b71f3 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 f15a2c397..c8929c5d4 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/125] 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 7f537e9c0..dbf8b5128 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 000000000..dcd1c58ee --- /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 000000000..9cb54bba9 --- /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 000000000..83151288f --- /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 c8929c5d4..bd5c8dd9e 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/125] 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 dbf8b5128..68ae1ba20 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 d22451696..db5d7883d 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 0612e1606..cad033a84 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/125] 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 68ae1ba20..7f57b4cef 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 b47bdfa42..df7ad91b4 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 a114c5473..fba37b919 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 edd1c1688..a7a715ed2 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 6f7ddb551..983c58ffc 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/125] 4.1 --- CHANGES.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7f57b4cef..cccc683fc 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/125] 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 94da11504..3ca897e3b 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 dcd1c58ee..6d9a64d0b 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/125] 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 cccc683fc..5e5c24ca8 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 000000000..5f53d92f9 --- /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 db5d7883d..dc84c8e2d 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 cad033a84..1669e3698 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/125] 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 5e5c24ca8..6f156f77f 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 5b2b9a46e..f3081a192 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 173ce535e..ffc13e9de 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 5f53d92f9..5026c7ac0 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/125] 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 f30377506..000000000 --- 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 bd5c8dd9e..a7bf69976 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/125] 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 5b07c032e..7adeb7518 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 df7ad91b4..7168bffff 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 000000000..599960266 --- /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/125] 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 6f156f77f..176ef6a03 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 c3349f60c..e04dedc0a 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 d7cbffff1..173285da5 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 37eebc7dc..844f7012e 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 000000000..39b79c60b --- /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 f3081a192..6fd35bb00 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 d489a10cf..ab3588cce 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 fba37b919..550cfd233 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/125] 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 176ef6a03..d5aa4c392 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 e04dedc0a..57db11da2 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 d5d103ea5..6cc200b1c 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 844f7012e..2bdb9a114 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 550cfd233..5034c2562 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 080fd1893..428f96857 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 7f384e287..f16bb3c27 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 63873e53a..aab32687d 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/125] 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 3ca897e3b..126b8632d 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 81e9b71f3..6f8977913 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 6d9a64d0b..816eda648 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 83151288f..90ee69163 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/125] 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 000000000..53830957d --- /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 315ccd83e..324e12905 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 6cc200b1c..f92841e9b 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 000000000..fa0055dba --- /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 5034c2562..0512e3ac1 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 fc08cc13b..15d3eae6e 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/125] 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 d5aa4c392..6a030b025 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 324e12905..be6a0ba17 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 eceb6139a..c251c31b6 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 0512e3ac1..224cb65e9 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 000000000..42b288682 --- /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/125] 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 6a030b025..048da2402 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 6fd35bb00..b6df8661d 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 224cb65e9..c0dc93e37 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 ffc13e9de..2e5cd09da 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/125] 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 048da2402..f776d3436 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 b6df8661d..fc3f7bd13 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 2e5cd09da..bc1f31a8d 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/125] 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 f776d3436..c8cb71067 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 2bdb9a114..0cb2490ca 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 c0dc93e37..e604d5dc0 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/125] 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 57db11da2..822f0d9ec 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/125] 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 c8cb71067..3cf46c2c1 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 eed9b7bd1..813247401 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 f92841e9b..f4e8c1f48 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 173285da5..767f1e4bf 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 14ca1f1a3..d2c8f3a5d 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 0ea6112e8..000000000 --- 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 d0aa6c78c..000000000 --- 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 0cb2490ca..81029285d 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 1b327f747..000000000 --- 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 00e11b4b2..000000000 --- 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 a3fa77bae..000000000
--- 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 aaaaf7ebc..7dae47585 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 e604d5dc0..2c2b7b90e 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 428f96857..aaac37522 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 aab32687d..52fd8077d 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 983c58ffc..bde0f8d71 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 1669e3698..7a573e00a 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 80289f112..5e9942446 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/125] 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 3cf46c2c1..7743e8785 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 972fee9cc..48481adb7 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 000000000..01f0d75da --- /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 f16bb3c27..0a7c63faf 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/125] 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 7743e8785..c7441d884 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 822f0d9ec..355b89433 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 80b61ce99..000000000 --- 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 63a2e9ae2..873e03d3b 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 0a7c63faf..540ca0faf 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/125] 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 c7441d884..fc3771644 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 355b89433..7a67d04e9 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 f4e8c1f48..6bbf4715a 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 ebdf7b065..c158aeb2e 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 48853b1f2..cd99901c8 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 767f1e4bf..7b2193854 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 81029285d..b1d60690f 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 8492d143b..1a7865cab 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 80614b53f..ab7e39f8c 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 48481adb7..46cf17508 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 8711b2d42..93f66eac8 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 2c2b7b90e..58fdc54b6 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 a7a715ed2..741237722 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 873e03d3b..322bffe4c 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 000000000..d02f8a240 --- /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 000000000..21f93026f --- /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 01f0d75da..f434302ba 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 aaac37522..02223ad56 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 52fd8077d..f7192d240 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 bde0f8d71..0170036f5 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/125] 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 c158aeb2e..9d4714108 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 289f60f79..412b10f66 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 1a7865cab..54f078fc5 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 93f66eac8..03c1b1d3a 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 d02f8a240..9442d760f 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/125] 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 fc3771644..023a13f72 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 7a67d04e9..57d0c4754 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 6bbf4715a..820ea2609 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 000000000..097e80aca --- /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/125] 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 023a13f72..49a534749 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 f7192d240..16fad5485 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 0170036f5..0ecd61e59 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/125] 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 49a534749..bd0edf11a 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 820ea2609..af322f69b 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/125] 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 bd0edf11a..c31e9f497 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 af322f69b..c08bf1631 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 58fdc54b6..873ac6258 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 741237722..3a3fa41de 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 16fad5485..cd3a03103 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 0ecd61e59..86f9920b1 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 000000000..66fa71949 --- /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 29e834e0d..befef3c7a 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 2f49d5695..d16a738ce 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/125] 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 c31e9f497..af012f3a1 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 57d0c4754..3541de16a 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 7168bffff..8a30186f8 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 000000000..e522f6f28 --- /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 46cf17508..1d4672492 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 f434302ba..0db530233 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 40c83eda8..d9751675f 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 540ca0faf..2bdb583ac 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 a7bf69976..b7b41a048 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/125] 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 cd3a03103..0c33f7508 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 86f9920b1..8a15db95c 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/125] 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 3541de16a..c8dfcd3c3 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 e522f6f28..1c901ed65 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 1d4672492..944f32557 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 0db530233..b01af77cc 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 2bdb583ac..e00b7be49 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/125] removed experimental disclaimer --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index c8dfcd3c3..e0a8db5fb 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/125] 6.0.0-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 11d840377..a2f0b2797 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/125] 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 02223ad56..c52308d52 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 126b8632d..24049dc0c 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 6f8977913..900bfc18b 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 816eda648..73c6b9962 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 9cb54bba9..e202cc109 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 90ee69163..feb571217 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/125] 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 af012f3a1..75cb7e0f2 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 7b2193854..b44a5fcff 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 54f078fc5..0c20a5138 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 873ac6258..495efc4b9 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 9442d760f..08da68bb4 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 0c33f7508..cd3a03103 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 8a15db95c..e8a23d76f 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 944f32557..17981d734 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 b01af77cc..d10fd4fe9 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/125] 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 b44a5fcff..864759ef3 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/125] 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 75cb7e0f2..c8ae4d203 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 be6a0ba17..64e97682b 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 9d4714108..b014d7113 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 cd99901c8..b07e20645 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 f5d6eabb9..d812cbc1e 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 ab3588cce..894855d47 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 495efc4b9..fa86f19da 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 3a3fa41de..0d32a36d1 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 322bffe4c..72fd77b20 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 c10aa5e6b..0894ed481 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 befef3c7a..70c34bc8f 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 d16a738ce..f5cc14c17 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 d9751675f..282be5f78 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/125] fixed CHANGES version

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

diff --git a/CHANGES.md b/CHANGES.md
index c8ae4d203..1325dd532 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/125] 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 1325dd532..cc2d66ea1 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 9054d1013..5dc36aeb7 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 f5cc14c17..d691b94cc 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/125] 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 e0a8db5fb..4afd717ea 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 0c20a5138..94396ff3d 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/125] 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 cc2d66ea1..2e1f43569 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 4afd717ea..229652f1b 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 206990e74..09b346404 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 000000000..649d7e00f --- /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 cd3a03103..c4d7d6c4a 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 e8a23d76f..75c189ffd 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 66fa71949..ea12f85d0 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/125] 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 412b10f66..f3b7b0ac6 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 94396ff3d..8167854c2 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 03c1b1d3a..ae35eca97 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 08da68bb4..c5b58f868 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/125] 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 2e1f43569..07a2d7777 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 229652f1b..622d45815 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 09b346404..bc6a47688 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 000000000..3a92f4f8a
--- /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 649d7e00f..b6ef12be1 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 000000000..66df54ea8
--- /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 000000000..4bee8df58
--- /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 c4d7d6c4a..79093101f 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 75c189ffd..2c9447610 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 ea12f85d0..6053ce51a 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/125] 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 07a2d7777..e3e5806f0 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 8a30186f8..022cce7e8 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/125] 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 e3e5806f0..833a45523 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 622d45815..1d43eacd3 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 f3b7b0ac6..0036be7bf 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 000000000..b0a2ee9eb
--- /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 000000000..3a8c6bf5a
--- /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/125] 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 833a45523..00f5e219f 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 1d43eacd3..e2a56ef1e 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 022cce7e8..3da4379f3 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 5efab25ba..1671bbdb6 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 12d06ba34..bc9c660ca 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 000000000..a6b8f0fcd
--- /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 000000000..83400afc1
--- /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 000000000..1cc6895f2
--- /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 000000000..7826118af
--- /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 000000000..c13583f6d --- /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 000000000..24f490efb --- /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 b7b41a048..8dac555cc 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/125] 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 00f5e219f..88225348c 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 e2a56ef1e..7339c831f 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 64e97682b..aab143daa 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 b07e20645..dd68d9a89 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 864759ef3..141e64425 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 b40c956a0..76d0f54f5 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 fc3f7bd13..6a3916593 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 6baa2b842..2324254d7 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 0036be7bf..2b847fa6c 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 8167854c2..346b149bf 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 ab7e39f8c..c3b07d591 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 7dae47585..e268fb7f7 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 fa86f19da..801422e25 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 c5b58f868..e270df5b5 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 21f93026f..1dc4fe598 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 efab2c9a7..e6173bca6 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 2c9447610..d0bce2abf 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 83400afc1..f0734d376 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 c13583f6d..a4f9dfa8e 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 134c289bf..a6d79205a 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 17981d734..0afc81773 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 d10fd4fe9..c4b9abf07 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 282be5f78..c229587b8 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/125] 7.0.0-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index a2f0b2797..868bb9b9a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=6.0.0-SNAPSHOT +version=7.0.0-SNAPSHOT

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