From 697fd66aae9beed107e13f49a741455f1d9d8dd9 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Sun, 18 Mar 2012 01:07:52 -0700 Subject: [PATCH 001/357] Initial commit, working with Maven Central --- .classpath | 9 + .gitignore | 40 ++++ .project | 39 ++++ ....springsource.sts.gradle.core.import.prefs | 9 + .../com.springsource.sts.gradle.core.prefs | 4 + .../com.springsource.sts.gradle.refresh.prefs | 9 + build.gradle | 48 +++++ codequality/checkstyle.xml | 188 ++++++++++++++++++ gradle/check.gradle | 16 ++ gradle/convention.gradle | 45 +++++ gradle/maven.gradle | 59 ++++++ gradle/netflix-oss.gradle | 1 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 39752 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 164 +++++++++++++++ gradlew.bat | 90 +++++++++ settings.gradle | 1 + template-client/.classpath | 11 + template-client/.project | 19 ++ .../com.springsource.sts.gradle.core.prefs | 4 + .../com.springsource.sts.gradle.refresh.prefs | 9 + .../netflix/template/client/TalkClient.class | Bin 0 -> 2256 bytes .../template/common/Conversation.class | Bin 0 -> 214 bytes .../netflix/template/common/Sentence.class | Bin 0 -> 784 bytes .../netflix/template/client/TalkClient.java | 36 ++++ .../netflix/template/common/Conversation.java | 6 + .../com/netflix/template/common/Sentence.java | 25 +++ template-server/.classpath | 12 ++ template-server/.project | 19 ++ .../com.springsource.sts.gradle.core.prefs | 4 + .../com.springsource.sts.gradle.refresh.prefs | 9 + .../netflix/template/server/TalkServer.class | Bin 0 -> 872 bytes .../netflix/template/server/TalkServer.java | 26 +++ .../src/main/webapp/WEB-INF/web.xml | 25 +++ 34 files changed, 933 insertions(+) create mode 100644 .classpath create mode 100644 .gitignore create mode 100644 .project create mode 100644 .settings/gradle/com.springsource.sts.gradle.core.import.prefs create mode 100644 .settings/gradle/com.springsource.sts.gradle.core.prefs create mode 100644 .settings/gradle/com.springsource.sts.gradle.refresh.prefs create mode 100644 build.gradle create mode 100644 codequality/checkstyle.xml create mode 100644 gradle/check.gradle create mode 100644 gradle/convention.gradle create mode 100644 gradle/maven.gradle create mode 100644 gradle/netflix-oss.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 template-client/.classpath create mode 100644 template-client/.project create mode 100644 template-client/.settings/gradle/com.springsource.sts.gradle.core.prefs create mode 100644 template-client/.settings/gradle/com.springsource.sts.gradle.refresh.prefs create mode 100644 template-client/bin/com/netflix/template/client/TalkClient.class create mode 100644 template-client/bin/com/netflix/template/common/Conversation.class create mode 100644 template-client/bin/com/netflix/template/common/Sentence.class create mode 100644 template-client/src/main/java/com/netflix/template/client/TalkClient.java create mode 100644 template-client/src/main/java/com/netflix/template/common/Conversation.java create mode 100644 template-client/src/main/java/com/netflix/template/common/Sentence.java create mode 100644 template-server/.classpath create mode 100644 template-server/.project create mode 100644 template-server/.settings/gradle/com.springsource.sts.gradle.core.prefs create mode 100644 template-server/.settings/gradle/com.springsource.sts.gradle.refresh.prefs create mode 100644 template-server/bin/com/netflix/template/server/TalkServer.class create mode 100644 template-server/src/main/java/com/netflix/template/server/TalkServer.java create mode 100644 template-server/src/main/webapp/WEB-INF/web.xml diff --git a/.classpath b/.classpath new file mode 100644 index 0000000000..b1ae8bae1c --- /dev/null +++ b/.classpath @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..618e741f86 --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +# Compiled source # +################### +*.com +*.class +*.dll +*.exe +*.o +*.so + +# Packages # +############ +# it's better to unpack these files and commit the raw source +# git has its own built in compression methods +*.7z +*.dmg +*.gz +*.iso +*.jar +*.rar +*.tar +*.zip + +# Logs and databases # +###################### +*.log + +# OS generated files # +###################### +.DS_Store* +ehthumbs.db +Icon? +Thumbs.db + +# Editor Files # +################ +*~ + +# Gradle Files # +################ +.gradle diff --git a/.project b/.project new file mode 100644 index 0000000000..f2d845e45a --- /dev/null +++ b/.project @@ -0,0 +1,39 @@ + + + gradle-template + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + com.springsource.sts.gradle.core.nature + org.eclipse.jdt.core.javanature + org.eclipse.jdt.groovy.core.groovyNature + + + + 1332049227118 + + 10 + + org.eclipse.ui.ide.orFilterMatcher + + + org.eclipse.ui.ide.multiFilter + 1.0-projectRelativePath-equals-true-false-template-server + + + org.eclipse.ui.ide.multiFilter + 1.0-projectRelativePath-equals-true-false-template-client + + + + + + diff --git a/.settings/gradle/com.springsource.sts.gradle.core.import.prefs b/.settings/gradle/com.springsource.sts.gradle.core.import.prefs new file mode 100644 index 0000000000..e86c91081f --- /dev/null +++ b/.settings/gradle/com.springsource.sts.gradle.core.import.prefs @@ -0,0 +1,9 @@ +#com.springsource.sts.gradle.core.preferences.GradleImportPreferences +#Sat Mar 17 22:40:13 PDT 2012 +enableAfterTasks=true +afterTasks=afterEclipseImport; +enableDependendencyManagement=true +enableBeforeTasks=true +projects=;template-client;template-server; +enableDSLD=true +beforeTasks=cleanEclipse;eclipse; diff --git a/.settings/gradle/com.springsource.sts.gradle.core.prefs b/.settings/gradle/com.springsource.sts.gradle.core.prefs new file mode 100644 index 0000000000..445ff6da6f --- /dev/null +++ b/.settings/gradle/com.springsource.sts.gradle.core.prefs @@ -0,0 +1,4 @@ +#com.springsource.sts.gradle.core.preferences.GradleProjectPreferences +#Sat Mar 17 22:40:29 PDT 2012 +com.springsource.sts.gradle.rootprojectloc= +com.springsource.sts.gradle.linkedresources= diff --git a/.settings/gradle/com.springsource.sts.gradle.refresh.prefs b/.settings/gradle/com.springsource.sts.gradle.refresh.prefs new file mode 100644 index 0000000000..01e59693e7 --- /dev/null +++ b/.settings/gradle/com.springsource.sts.gradle.refresh.prefs @@ -0,0 +1,9 @@ +#com.springsource.sts.gradle.core.actions.GradleRefreshPreferences +#Sat Mar 17 22:40:27 PDT 2012 +enableAfterTasks=true +afterTasks=afterEclipseImport; +useHierarchicalNames=false +enableBeforeTasks=true +addResourceFilters=true +enableDSLD=true +beforeTasks=cleanEclipse;eclipse; diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..5297034a51 --- /dev/null +++ b/build.gradle @@ -0,0 +1,48 @@ +// Establish version and status +ext.releaseVersion = '1.1.3' // TEMPLATE: Set to latest release +ext.githubProjectName = rootProject.name // TEMPLATE: change to match github project, if it doesn't match project name + +apply from: file('gradle/convention.gradle') +apply from: file('gradle/maven.gradle') +apply from: file('gradle/check.gradle') + +subprojects +{ + group = 'com.netflix' + + repositories + { + mavenCentral() + } + + dependencies + { + compile 'javax.ws.rs:jsr311-api:1.1.1' + compile 'com.sun.jersey:jersey-core:1.11' + testCompile 'org.testng:testng:6.1.1' + testCompile 'org.mockito:mockito-core:1.8.5' + } +} + +project(':template-client') +{ + dependencies + { + compile 'org.slf4j:slf4j-api:1.6.3' + compile 'com.sun.jersey:jersey-client:1.11' + } +} + +project(':template-server') +{ + apply plugin: 'war' + apply plugin: 'jetty' + dependencies + { + compile 'com.sun.jersey:jersey-server:1.11' + compile 'com.sun.jersey:jersey-servlet:1.11' + compile project(':template-client') + testCompile 'org.mockito:mockito-core:1.8.5' + } +} + diff --git a/codequality/checkstyle.xml b/codequality/checkstyle.xml new file mode 100644 index 0000000000..3c8a8e6c75 --- /dev/null +++ b/codequality/checkstyle.xml @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle/check.gradle b/gradle/check.gradle new file mode 100644 index 0000000000..cf6f0461ae --- /dev/null +++ b/gradle/check.gradle @@ -0,0 +1,16 @@ +subprojects { + // Checkstyle + apply plugin: 'checkstyle' + tasks.withType(Checkstyle) { ignoreFailures = true } + checkstyle { + ignoreFailures = true // Waiting on GRADLE-2163 + configFile = rootProject.file('codequality/checkstyle.xml') + } + + // FindBugs + apply plugin: 'findbugs' + + // PMD + apply plugin: 'pmd' + +} diff --git a/gradle/convention.gradle b/gradle/convention.gradle new file mode 100644 index 0000000000..a3fc06dd04 --- /dev/null +++ b/gradle/convention.gradle @@ -0,0 +1,45 @@ + +ext.performingRelease = project.hasProperty('release') && Boolean.parseBoolean(project.release) +def versionPostfix = performingRelease?'':'-SNAPSHOT' +version = "${releaseVersion}${versionPostfix}" +status = performingRelease?'release':'snapshot' + +subprojects +{ + apply plugin: 'java' // Plugin as major conventions + + version = rootProject.version + + sourceCompatibility = 1.6 + + // GRADLE-2087 workaround, perform after java plugin + status = rootProject.status + + task sourcesJar(type: Jar, dependsOn:classes) { + classifier = 'sources' + from sourceSets.main.allSource + } + + task javadocJar(type: Jar, dependsOn:javadoc) { + classifier = 'javadoc' + from javadoc.destinationDir + } + + artifacts { + archives jar + archives sourcesJar + archives javadocJar + } +} + +task aggregateJavadoc(type: Javadoc) { + description = 'Aggregate all subproject docs into a single docs directory' + source subprojects.collect {project -> project.sourceSets.main.allJava } + classpath = files(subprojects.collect {project -> project.sourceSets.main.compileClasspath}) + destinationDir = new File(projectDir, 'doc') +} + +// Generate wrapper, which is distributed as part of source to alleviate the need of installing gradle +task createWrapper(type: Wrapper) { + gradleVersion = '1.0-milestone-9' +} diff --git a/gradle/maven.gradle b/gradle/maven.gradle new file mode 100644 index 0000000000..8639564ce4 --- /dev/null +++ b/gradle/maven.gradle @@ -0,0 +1,59 @@ +// Maven side of things +subprojects { + apply plugin: 'maven' // Java plugin has to have been already applied for the conf2scope mappings to work + apply plugin: 'signing' + + signing { + required rootProject.performingRelease + sign configurations.archives + } + + /** + * Publishing to Maven Central example provided from http://jedicoder.blogspot.com/2011/11/automated-gradle-project-deployment-to.html + */ + task uploadMavenCentral(type:Upload) { + configuration = configurations.archives + dependsOn signArchives + doFirst { + repositories.mavenDeployer { + beforeDeployment { org.gradle.api.artifacts.maven.MavenDeployment deployment -> signing.signPom(deployment) } + + // To test deployment locally, use the following instead of oss.sonatype.org + //repository(url: "file://localhost/${rootProject.rootDir}/repo") + + repository(url: 'http://oss.sonatype.org/services/local/staging/deply/maven2/') { + authentication(userName: rootProject.sonatypeUsername, password: rootProject.sonatypePassword) + } + + // Prevent datastamp from being appending to artifacts during deployment + uniqueVersion = false + + // Closure to configure all the POM with extra info, common to all projects + pom.project { + parent { + groupId 'org.sonatype.oss' + artifactId 'oss-parent' + version '7' + } + url "https://github.com/Netflix/${rootProject.ext.githubProjectName}" + licenses { + license { + name 'The Apache Software License, Version 2.0' + url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + distribution 'repo' + } + } + scm { + connection "scm:git:git@github.com:Netflix/${rootProject.ext.githubProjectName}.git" + url "scm:git:git@github.com:Netflix/${rootProject.ext.githubProjectName}.git" + developerConnection "scm:git:git@github.com:Netflix/${rootProject.ext.githubProjectName}.git" + } + issueManagement { + system 'github' + url 'https://github.com/Netflix/${rootProject.ext.githubProjectName}/issues' + } + } + } + } + } +} diff --git a/gradle/netflix-oss.gradle b/gradle/netflix-oss.gradle new file mode 100644 index 0000000000..a87bc54efe --- /dev/null +++ b/gradle/netflix-oss.gradle @@ -0,0 +1 @@ +apply from: 'http://artifacts.netflix.com/gradle-netflix-local/artifactory.gradle' diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..2cb758a1c693a7cc7a489e4450ca1c700ac83049 GIT binary patch literal 39752 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" +APP_HOME="`pwd -P`" +cd "$SAVED" + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query businessSystem maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + JAVA_OPTS="$JAVA_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000000..8a0b282aa6 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000000..350f2f1b49 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include 'template-client','template-server' diff --git a/template-client/.classpath b/template-client/.classpath new file mode 100644 index 0000000000..cd1d475d6b --- /dev/null +++ b/template-client/.classpath @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/template-client/.project b/template-client/.project new file mode 100644 index 0000000000..66279e9c19 --- /dev/null +++ b/template-client/.project @@ -0,0 +1,19 @@ + + + template-client + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + com.springsource.sts.gradle.core.nature + org.eclipse.jdt.core.javanature + org.eclipse.jdt.groovy.core.groovyNature + + diff --git a/template-client/.settings/gradle/com.springsource.sts.gradle.core.prefs b/template-client/.settings/gradle/com.springsource.sts.gradle.core.prefs new file mode 100644 index 0000000000..1e7f022837 --- /dev/null +++ b/template-client/.settings/gradle/com.springsource.sts.gradle.core.prefs @@ -0,0 +1,4 @@ +#com.springsource.sts.gradle.core.preferences.GradleProjectPreferences +#Sat Mar 17 22:40:30 PDT 2012 +com.springsource.sts.gradle.rootprojectloc=.. +com.springsource.sts.gradle.linkedresources= diff --git a/template-client/.settings/gradle/com.springsource.sts.gradle.refresh.prefs b/template-client/.settings/gradle/com.springsource.sts.gradle.refresh.prefs new file mode 100644 index 0000000000..394fb107f1 --- /dev/null +++ b/template-client/.settings/gradle/com.springsource.sts.gradle.refresh.prefs @@ -0,0 +1,9 @@ +#com.springsource.sts.gradle.core.actions.GradleRefreshPreferences +#Sat Mar 17 22:40:30 PDT 2012 +enableAfterTasks=true +afterTasks=afterEclipseImport; +useHierarchicalNames=false +enableBeforeTasks=true +addResourceFilters=true +enableDSLD=true +beforeTasks=cleanEclipse;eclipse; diff --git a/template-client/bin/com/netflix/template/client/TalkClient.class b/template-client/bin/com/netflix/template/client/TalkClient.class new file mode 100644 index 0000000000000000000000000000000000000000..90bbaeb3532635ac2638cc61156bb545c24ba039 GIT binary patch literal 2256 zcma)7YgZdp6y2AkOfnq>LJ38x0_72)Fce=%uxhETwQ0cy!B}5IatT8xGjTGZ?Kgjt zx-@jzrOVI$D3|-r3?)3vhndW|x%cd|&)MhPfB*aQZvgYy)zBd@UiNCHD}yb^erg7? zR(GsGnq|k9ZeXri&g13qQ$tdqd&k~P&T}#UzP$B1$DB~bj=-Zk-`9=uBQi zR$%sY*ITv|NZ%}Y)hgq>9a?Ez#v2+24kfertijA17{nDu?ll7vjd zF@Z@9xx-Y#ni!bEw1Bp2IZe5;MU}PJEz(YY@^~qAjABOd(^D`7E|>uYs1~mq6zn@J zkX5fsw<0jp?r>iS#~j9yGH;`J&%pcmKp+)((SCtTtnyKE+}n*04J?AXOZhLwnm-Zn z^y0S*I1Pc13}{FRbQNq@K4{i9rN3rvI@DUG;FT?B-STZ^$BW5e+itM8!jiqZJyom@ z-9&AcInGLHJ8WcukuzivE1TY`zLWuF%NXKbI37jR;zdph>6T?ag}da(3ORNZiTSkV z=(vab8VUkq=Sr4=2Ut;sE|+DUjc_aX=s26RnzJqOqx<#O@Fja%;lGN6jv~I&uttTQ zE8Kku1MApe^HwEe9&6XivST$Ghr}N1UqGkNN!n?Ez6frq{ECB@vVXH6kcaV zI#La+WX-ZUV6!J?Ydr-^U}`%E>WikSbmDA#jbL$MZei}_%%5pUpQGBOH|(LqbI5=LofDw0rkCR?U#@{~aEGBWG!TUAf?aSAP4xT^k6ugf literal 0 HcmV?d00001 diff --git a/template-client/bin/com/netflix/template/common/Conversation.class b/template-client/bin/com/netflix/template/common/Conversation.class new file mode 100644 index 0000000000000000000000000000000000000000..86d42f57590c9fbee4a60450fd6eca4121b4cf6a GIT binary patch literal 214 zcmaKmF%H5o6hoZ?ZRrHJC`%);G9ob{G4uo>`mIu>KPZI4*%&wghe9M96O$!dw%_~n zd;!>^Dv$}(+KrMabk;m%pz&f=AQ{ckvD`bJ$X``3jtk5MR)d<9w2FIqIuE3SK-qhu zV7QN4_2&3*t|bn{ns%|(DNlE@R-kI#&1*UsO9JcP%O<_$0s^y03}lgDfgFjXNE(we H`B;7deJVPB literal 0 HcmV?d00001 diff --git a/template-client/bin/com/netflix/template/common/Sentence.class b/template-client/bin/com/netflix/template/common/Sentence.class new file mode 100644 index 0000000000000000000000000000000000000000..0083f334776617dd596f960095e6ab566be6b905 GIT binary patch literal 784 zcma)(TT2^36vzLQY%a!Jyhd%G3Z*0|bih6;LMinrR4@{KovhP1VRuH7iRNQjPy`?P z0s5iDb9Q4QUci?**ZrU0Is5<(Z4Lz)E{w>-eFu{T+e)uCd1N31l11u0Zh9o$3;@ zSS+J}qCl-}to}WYdwO`JdZ~;HRn%2O!|^m3_%kyS_|kq4D2-ijyo70X7eJI{=}D1)vPK{;^+21c=PK1?bcIx^)LToG z>Qm)ZiDxhtL#$$rYU`xQah)vd%cRDD*I2%yL<-0)pif?d+nB-aQ8&ZopMj<8ZP4h= VCs6t6dJK?4WvI>*w`N!$fCt$xmcRf2 literal 0 HcmV?d00001 diff --git a/template-client/src/main/java/com/netflix/template/client/TalkClient.java b/template-client/src/main/java/com/netflix/template/client/TalkClient.java new file mode 100644 index 0000000000..c3ebf86090 --- /dev/null +++ b/template-client/src/main/java/com/netflix/template/client/TalkClient.java @@ -0,0 +1,36 @@ +package com.netflix.template.client; + +import com.netflix.template.common.Conversation; +import com.netflix.template.common.Sentence; +import com.sun.jersey.api.client.Client; +import com.sun.jersey.api.client.WebResource; +import com.sun.jersey.api.client.filter.LoggingFilter; + +import javax.ws.rs.core.MediaType; + +public class TalkClient implements Conversation { + + WebResource webResource; + + TalkClient(String location) { + Client client = Client.create(); + client.addFilter(new LoggingFilter(System.out)); + webResource = client.resource(location + "/talk"); + } + + public Sentence greeting() { + Sentence s = webResource.accept(MediaType.APPLICATION_XML).get(Sentence.class); + return s; + } + + public Sentence farewell() { + Sentence s = webResource.accept(MediaType.APPLICATION_XML).delete(Sentence.class); + return s; + } + + public static void main(String[] args) { + TalkClient remote = new TalkClient("http://localhost:8080/template-server/rest"); + System.out.println(remote.greeting().getWhole()); + System.out.println(remote.farewell().getWhole()); + } +} diff --git a/template-client/src/main/java/com/netflix/template/common/Conversation.java b/template-client/src/main/java/com/netflix/template/common/Conversation.java new file mode 100644 index 0000000000..b85e23e98b --- /dev/null +++ b/template-client/src/main/java/com/netflix/template/common/Conversation.java @@ -0,0 +1,6 @@ +package com.netflix.template.common; + +public interface Conversation { + Sentence greeting(); + Sentence farewell(); +} \ No newline at end of file diff --git a/template-client/src/main/java/com/netflix/template/common/Sentence.java b/template-client/src/main/java/com/netflix/template/common/Sentence.java new file mode 100644 index 0000000000..bf561a6d5a --- /dev/null +++ b/template-client/src/main/java/com/netflix/template/common/Sentence.java @@ -0,0 +1,25 @@ +package com.netflix.template.common; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement +public class Sentence { + private String whole; + + private Sentence() { + }; + + public Sentence(String whole) { + this.whole = whole; + } + + @XmlElement + public String getWhole() { + return whole; + } + + public void setWhole(String whole) { + this.whole = whole; + } +} \ No newline at end of file diff --git a/template-server/.classpath b/template-server/.classpath new file mode 100644 index 0000000000..09505c5c72 --- /dev/null +++ b/template-server/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/template-server/.project b/template-server/.project new file mode 100644 index 0000000000..0b2a3869fa --- /dev/null +++ b/template-server/.project @@ -0,0 +1,19 @@ + + + template-server + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + com.springsource.sts.gradle.core.nature + org.eclipse.jdt.core.javanature + org.eclipse.jdt.groovy.core.groovyNature + + diff --git a/template-server/.settings/gradle/com.springsource.sts.gradle.core.prefs b/template-server/.settings/gradle/com.springsource.sts.gradle.core.prefs new file mode 100644 index 0000000000..1e7f022837 --- /dev/null +++ b/template-server/.settings/gradle/com.springsource.sts.gradle.core.prefs @@ -0,0 +1,4 @@ +#com.springsource.sts.gradle.core.preferences.GradleProjectPreferences +#Sat Mar 17 22:40:30 PDT 2012 +com.springsource.sts.gradle.rootprojectloc=.. +com.springsource.sts.gradle.linkedresources= diff --git a/template-server/.settings/gradle/com.springsource.sts.gradle.refresh.prefs b/template-server/.settings/gradle/com.springsource.sts.gradle.refresh.prefs new file mode 100644 index 0000000000..394fb107f1 --- /dev/null +++ b/template-server/.settings/gradle/com.springsource.sts.gradle.refresh.prefs @@ -0,0 +1,9 @@ +#com.springsource.sts.gradle.core.actions.GradleRefreshPreferences +#Sat Mar 17 22:40:30 PDT 2012 +enableAfterTasks=true +afterTasks=afterEclipseImport; +useHierarchicalNames=false +enableBeforeTasks=true +addResourceFilters=true +enableDSLD=true +beforeTasks=cleanEclipse;eclipse; diff --git a/template-server/bin/com/netflix/template/server/TalkServer.class b/template-server/bin/com/netflix/template/server/TalkServer.class new file mode 100644 index 0000000000000000000000000000000000000000..f534d0627d9bc6892160f0803ec67e0b1aa1bf62 GIT binary patch literal 872 zcmah{%Wl&^6g`tBv2jyEO`p6ArAk>a5@HvGgi0t23n~&tLaZjvG@UY@iR?)l{t8x= zK;i@VD8#i}K?0Uoc5jFUaVL|p7Eba^rc;^n zp3on=h3lcpaP3q~1=qri_}js$jGc!%L#q^lf{8W!z#0O|gj3cq)SoG%+;fJd)_$L% zdSHh#z!H`l@Zd8vBW2{9NivXWPYkqV2qPN{-506K@0Y=hn*hfZ!E-) zQahZ)GNT{0sn8RurYXi_t>OZM&l2rni($94ioewOxIr+lrPemUCT`^oyUnoPDkv{z z(se0S*v>oaAB$9;Q8vTcf~c3BsMG7Tee5uJht>`UpGa5GwUacKuTIZIBU^iPj^GP96*TCq7r_;*kl(mSz*RKq zMhk{j$_mL3$zCVBM$z>TU>PJaRaP9Q;PUvw(c}zsUDW Ykhe;ZE4W|qKPXf$liJ-}afXM#0MazXfdBvi literal 0 HcmV?d00001 diff --git a/template-server/src/main/java/com/netflix/template/server/TalkServer.java b/template-server/src/main/java/com/netflix/template/server/TalkServer.java new file mode 100644 index 0000000000..a856ce88a2 --- /dev/null +++ b/template-server/src/main/java/com/netflix/template/server/TalkServer.java @@ -0,0 +1,26 @@ +package com.netflix.template.server; + +import com.netflix.template.common.Conversation; +import com.netflix.template.common.Sentence; + +import javax.ws.rs.GET; +import javax.ws.rs.DELETE; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +@Path("/talk") +public class TalkServer implements Conversation { + + @GET + @Produces(MediaType.APPLICATION_XML) + public Sentence greeting() { + return new Sentence("Hello"); + } + + @DELETE + @Produces(MediaType.APPLICATION_XML) + public Sentence farewell() { + return new Sentence("Goodbye"); + } +} \ No newline at end of file diff --git a/template-server/src/main/webapp/WEB-INF/web.xml b/template-server/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000..273135a292 --- /dev/null +++ b/template-server/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,25 @@ + + + + Jersey REST Service + + com.sun.jersey.spi.container.servlet.ServletContainer + + + com.sun.jersey.config.property.packages + com.netflix.template.server + + + com.sun.jersey.api.json.POJOMappingFeature + true + + 1 + + + + Jersey REST Service + /rest/* + + From 52bd53f5ea5eec84818d65b40e81d0a82ada6ba8 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Mon, 2 Apr 2012 16:18:03 -0700 Subject: [PATCH 002/357] Restructure into smaller files --- .classpath | 9 - .gitignore | 23 ++ .project | 39 ---- ....springsource.sts.gradle.core.import.prefs | 9 - .../com.springsource.sts.gradle.core.prefs | 4 - .../com.springsource.sts.gradle.refresh.prefs | 9 - LICENSE | 202 ++++++++++++++++++ build.gradle | 24 +-- codequality/HEADER | 13 ++ codequality/checkstyle.xml | 1 + gradle/check.gradle | 2 + gradle/license.gradle | 5 + gradle/local.gradle | 1 + gradle/maven.gradle | 2 +- .../netflix/template/client/TalkClient.class | Bin 2256 -> 0 bytes .../netflix/template/common/Sentence.class | Bin 784 -> 0 bytes .../netflix/template/client/TalkClient.java | 20 +- .../netflix/template/common/Conversation.java | 17 +- .../com/netflix/template/common/Sentence.java | 16 +- 19 files changed, 306 insertions(+), 90 deletions(-) delete mode 100644 .classpath delete mode 100644 .project delete mode 100644 .settings/gradle/com.springsource.sts.gradle.core.import.prefs delete mode 100644 .settings/gradle/com.springsource.sts.gradle.core.prefs delete mode 100644 .settings/gradle/com.springsource.sts.gradle.refresh.prefs create mode 100644 LICENSE create mode 100644 codequality/HEADER create mode 100644 gradle/license.gradle create mode 100644 gradle/local.gradle delete mode 100644 template-client/bin/com/netflix/template/client/TalkClient.class delete mode 100644 template-client/bin/com/netflix/template/common/Sentence.class diff --git a/.classpath b/.classpath deleted file mode 100644 index b1ae8bae1c..0000000000 --- a/.classpath +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/.gitignore b/.gitignore index 618e741f86..313af3cb82 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,26 @@ Thumbs.db # Gradle Files # ################ .gradle + +# Build output directies +/target +*/target +/build +*/build +# +# # IntelliJ specific files/directories +out +.idea +*.ipr +*.iws +*.iml +atlassian-ide-plugin.xml + +# Eclipse specific files/directories +.classpath +.project +.settings +.metadata + +# NetBeans specific files/directories +.nbattrs diff --git a/.project b/.project deleted file mode 100644 index f2d845e45a..0000000000 --- a/.project +++ /dev/null @@ -1,39 +0,0 @@ - - - gradle-template - - - - - - org.eclipse.jdt.core.javabuilder - - - - - - com.springsource.sts.gradle.core.nature - org.eclipse.jdt.core.javanature - org.eclipse.jdt.groovy.core.groovyNature - - - - 1332049227118 - - 10 - - org.eclipse.ui.ide.orFilterMatcher - - - org.eclipse.ui.ide.multiFilter - 1.0-projectRelativePath-equals-true-false-template-server - - - org.eclipse.ui.ide.multiFilter - 1.0-projectRelativePath-equals-true-false-template-client - - - - - - diff --git a/.settings/gradle/com.springsource.sts.gradle.core.import.prefs b/.settings/gradle/com.springsource.sts.gradle.core.import.prefs deleted file mode 100644 index e86c91081f..0000000000 --- a/.settings/gradle/com.springsource.sts.gradle.core.import.prefs +++ /dev/null @@ -1,9 +0,0 @@ -#com.springsource.sts.gradle.core.preferences.GradleImportPreferences -#Sat Mar 17 22:40:13 PDT 2012 -enableAfterTasks=true -afterTasks=afterEclipseImport; -enableDependendencyManagement=true -enableBeforeTasks=true -projects=;template-client;template-server; -enableDSLD=true -beforeTasks=cleanEclipse;eclipse; diff --git a/.settings/gradle/com.springsource.sts.gradle.core.prefs b/.settings/gradle/com.springsource.sts.gradle.core.prefs deleted file mode 100644 index 445ff6da6f..0000000000 --- a/.settings/gradle/com.springsource.sts.gradle.core.prefs +++ /dev/null @@ -1,4 +0,0 @@ -#com.springsource.sts.gradle.core.preferences.GradleProjectPreferences -#Sat Mar 17 22:40:29 PDT 2012 -com.springsource.sts.gradle.rootprojectloc= -com.springsource.sts.gradle.linkedresources= diff --git a/.settings/gradle/com.springsource.sts.gradle.refresh.prefs b/.settings/gradle/com.springsource.sts.gradle.refresh.prefs deleted file mode 100644 index 01e59693e7..0000000000 --- a/.settings/gradle/com.springsource.sts.gradle.refresh.prefs +++ /dev/null @@ -1,9 +0,0 @@ -#com.springsource.sts.gradle.core.actions.GradleRefreshPreferences -#Sat Mar 17 22:40:27 PDT 2012 -enableAfterTasks=true -afterTasks=afterEclipseImport; -useHierarchicalNames=false -enableBeforeTasks=true -addResourceFilters=true -enableDSLD=true -beforeTasks=cleanEclipse;eclipse; diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..7f8ced0d1f --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2012 Netflix, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/build.gradle b/build.gradle index 5297034a51..9eef3329e1 100644 --- a/build.gradle +++ b/build.gradle @@ -5,18 +5,16 @@ ext.githubProjectName = rootProject.name // TEMPLATE: change to match github pro apply from: file('gradle/convention.gradle') apply from: file('gradle/maven.gradle') apply from: file('gradle/check.gradle') +apply from: file('gradle/license.gradle') -subprojects -{ - group = 'com.netflix' +subprojects { + group = 'com.netflix.osstemplate' // TEMPLATE: Set to organization of project - repositories - { + repositories { mavenCentral() } - dependencies - { + dependencies { compile 'javax.ws.rs:jsr311-api:1.1.1' compile 'com.sun.jersey:jersey-core:1.11' testCompile 'org.testng:testng:6.1.1' @@ -24,21 +22,17 @@ subprojects } } -project(':template-client') -{ - dependencies - { +project(':template-client') { + dependencies { compile 'org.slf4j:slf4j-api:1.6.3' compile 'com.sun.jersey:jersey-client:1.11' } } -project(':template-server') -{ +project(':template-server') { apply plugin: 'war' apply plugin: 'jetty' - dependencies - { + dependencies { compile 'com.sun.jersey:jersey-server:1.11' compile 'com.sun.jersey:jersey-servlet:1.11' compile project(':template-client') diff --git a/codequality/HEADER b/codequality/HEADER new file mode 100644 index 0000000000..b27b192925 --- /dev/null +++ b/codequality/HEADER @@ -0,0 +1,13 @@ + Copyright 2012 Netflix, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/codequality/checkstyle.xml b/codequality/checkstyle.xml index 3c8a8e6c75..481d2829fd 100644 --- a/codequality/checkstyle.xml +++ b/codequality/checkstyle.xml @@ -50,6 +50,7 @@ + diff --git a/gradle/check.gradle b/gradle/check.gradle index cf6f0461ae..0f80516d45 100644 --- a/gradle/check.gradle +++ b/gradle/check.gradle @@ -9,8 +9,10 @@ subprojects { // FindBugs apply plugin: 'findbugs' + //tasks.withType(Findbugs) { reports.html.enabled true } // PMD apply plugin: 'pmd' + //tasks.withType(Pmd) { reports.html.enabled true } } diff --git a/gradle/license.gradle b/gradle/license.gradle new file mode 100644 index 0000000000..9d04830321 --- /dev/null +++ b/gradle/license.gradle @@ -0,0 +1,5 @@ +buildscript { + dependencies { classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.4' } +} + +apply plugin: 'license' \ No newline at end of file diff --git a/gradle/local.gradle b/gradle/local.gradle new file mode 100644 index 0000000000..6f2d204b8a --- /dev/null +++ b/gradle/local.gradle @@ -0,0 +1 @@ +apply from: 'file://Users/jryan/Workspaces/jryan_build/Tools/nebula-boot/artifactory.gradle' diff --git a/gradle/maven.gradle b/gradle/maven.gradle index 8639564ce4..cb75dfb637 100644 --- a/gradle/maven.gradle +++ b/gradle/maven.gradle @@ -4,7 +4,7 @@ subprojects { apply plugin: 'signing' signing { - required rootProject.performingRelease + required { performingRelease && gradle.taskGraph.hasTask("uploadMavenCentral")} sign configurations.archives } diff --git a/template-client/bin/com/netflix/template/client/TalkClient.class b/template-client/bin/com/netflix/template/client/TalkClient.class deleted file mode 100644 index 90bbaeb3532635ac2638cc61156bb545c24ba039..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2256 zcma)7YgZdp6y2AkOfnq>LJ38x0_72)Fce=%uxhETwQ0cy!B}5IatT8xGjTGZ?Kgjt zx-@jzrOVI$D3|-r3?)3vhndW|x%cd|&)MhPfB*aQZvgYy)zBd@UiNCHD}yb^erg7? zR(GsGnq|k9ZeXri&g13qQ$tdqd&k~P&T}#UzP$B1$DB~bj=-Zk-`9=uBQi zR$%sY*ITv|NZ%}Y)hgq>9a?Ez#v2+24kfertijA17{nDu?ll7vjd zF@Z@9xx-Y#ni!bEw1Bp2IZe5;MU}PJEz(YY@^~qAjABOd(^D`7E|>uYs1~mq6zn@J zkX5fsw<0jp?r>iS#~j9yGH;`J&%pcmKp+)((SCtTtnyKE+}n*04J?AXOZhLwnm-Zn z^y0S*I1Pc13}{FRbQNq@K4{i9rN3rvI@DUG;FT?B-STZ^$BW5e+itM8!jiqZJyom@ z-9&AcInGLHJ8WcukuzivE1TY`zLWuF%NXKbI37jR;zdph>6T?ag}da(3ORNZiTSkV z=(vab8VUkq=Sr4=2Ut;sE|+DUjc_aX=s26RnzJqOqx<#O@Fja%;lGN6jv~I&uttTQ zE8Kku1MApe^HwEe9&6XivST$Ghr}N1UqGkNN!n?Ez6frq{ECB@vVXH6kcaV zI#La+WX-ZUV6!J?Ydr-^U}`%E>WikSbmDA#jbL$MZei}_%%5pUpQGBOH|(LqbI5=LofDw0rkCR?U#@{~aEGBWG!TUAf?aSAP4xT^k6ugf diff --git a/template-client/bin/com/netflix/template/common/Sentence.class b/template-client/bin/com/netflix/template/common/Sentence.class deleted file mode 100644 index 0083f334776617dd596f960095e6ab566be6b905..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 784 zcma)(TT2^36vzLQY%a!Jyhd%G3Z*0|bih6;LMinrR4@{KovhP1VRuH7iRNQjPy`?P z0s5iDb9Q4QUci?**ZrU0Is5<(Z4Lz)E{w>-eFu{T+e)uCd1N31l11u0Zh9o$3;@ zSS+J}qCl-}to}WYdwO`JdZ~;HRn%2O!|^m3_%kyS_|kq4D2-ijyo70X7eJI{=}D1)vPK{;^+21c=PK1?bcIx^)LToG z>Qm)ZiDxhtL#$$rYU`xQah)vd%cRDD*I2%yL<-0)pif?d+nB-aQ8&ZopMj<8ZP4h= VCs6t6dJK?4WvI>*w`N!$fCt$xmcRf2 diff --git a/template-client/src/main/java/com/netflix/template/client/TalkClient.java b/template-client/src/main/java/com/netflix/template/client/TalkClient.java index c3ebf86090..fc9d20d33d 100644 --- a/template-client/src/main/java/com/netflix/template/client/TalkClient.java +++ b/template-client/src/main/java/com/netflix/template/client/TalkClient.java @@ -8,26 +8,42 @@ import javax.ws.rs.core.MediaType; +/** + * Delegates to remote TalkServer over REST. + * @author jryan + * + */ public class TalkClient implements Conversation { - WebResource webResource; + private WebResource webResource; - TalkClient(String location) { + /** + * Instantiate client. + * + * @param location URL to the base of resources, e.g. http://localhost:8080/template-server/rest + */ + public TalkClient(String location) { Client client = Client.create(); client.addFilter(new LoggingFilter(System.out)); webResource = client.resource(location + "/talk"); } + @Override public Sentence greeting() { Sentence s = webResource.accept(MediaType.APPLICATION_XML).get(Sentence.class); return s; } + @Override public Sentence farewell() { Sentence s = webResource.accept(MediaType.APPLICATION_XML).delete(Sentence.class); return s; } + /** + * Tests out client. + * @param args Not applicable + */ public static void main(String[] args) { TalkClient remote = new TalkClient("http://localhost:8080/template-server/rest"); System.out.println(remote.greeting().getWhole()); diff --git a/template-client/src/main/java/com/netflix/template/common/Conversation.java b/template-client/src/main/java/com/netflix/template/common/Conversation.java index b85e23e98b..c190f03bb7 100644 --- a/template-client/src/main/java/com/netflix/template/common/Conversation.java +++ b/template-client/src/main/java/com/netflix/template/common/Conversation.java @@ -1,6 +1,21 @@ package com.netflix.template.common; +/** + * Hold a conversation. + * @author jryan + * + */ public interface Conversation { + + /** + * Initiates a conversation. + * @return Sentence words from geeting + */ Sentence greeting(); + + /** + * End the conversation. + * @return + */ Sentence farewell(); -} \ No newline at end of file +} diff --git a/template-client/src/main/java/com/netflix/template/common/Sentence.java b/template-client/src/main/java/com/netflix/template/common/Sentence.java index bf561a6d5a..616f72efb0 100644 --- a/template-client/src/main/java/com/netflix/template/common/Sentence.java +++ b/template-client/src/main/java/com/netflix/template/common/Sentence.java @@ -3,17 +3,31 @@ import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; +/** + * Container for words going back and forth. + * @author jryan + * + */ @XmlRootElement public class Sentence { private String whole; + @SuppressWarnings("unused") private Sentence() { }; + /** + * Initialize sentence. + * @param whole + */ public Sentence(String whole) { this.whole = whole; } + /** + * whole getter. + * @return + */ @XmlElement public String getWhole() { return whole; @@ -22,4 +36,4 @@ public String getWhole() { public void setWhole(String whole) { this.whole = whole; } -} \ No newline at end of file +} From b5b2f5ef9e908a3c53e4afe017a60f2b878a93b3 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Mon, 9 Apr 2012 10:52:02 -0700 Subject: [PATCH 003/357] Correct artifacts, moved pom to more visible area --- build.gradle | 31 ++++++++++++++++++++++++++----- gradle/convention.gradle | 1 - gradle/license.gradle | 4 ++-- gradle/maven.gradle | 14 ++------------ gradle/release.gradle | 6 ++++++ 5 files changed, 36 insertions(+), 20 deletions(-) create mode 100644 gradle/release.gradle diff --git a/build.gradle b/build.gradle index 9eef3329e1..c0d2d5e7ff 100644 --- a/build.gradle +++ b/build.gradle @@ -2,18 +2,39 @@ ext.releaseVersion = '1.1.3' // TEMPLATE: Set to latest release ext.githubProjectName = rootProject.name // TEMPLATE: change to match github project, if it doesn't match project name +buildscript { + repositories { mavenCentral() } +} + +allprojects { + repositories { mavenCentral() } +} + +//apply from: file('gradle/release.gradle') // Not fully tested apply from: file('gradle/convention.gradle') apply from: file('gradle/maven.gradle') apply from: file('gradle/check.gradle') -apply from: file('gradle/license.gradle') +//apply from: file('gradle/license.gradle') // Waiting for re-release subprojects { - group = 'com.netflix.osstemplate' // TEMPLATE: Set to organization of project - - repositories { - mavenCentral() + // Closure to configure all the POM with extra info, common to all projects + pom { + project { + url "https://github.com/Netflix/${rootProject.githubProjectName}" + scm { + connection "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" + url "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" + developerConnection "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" + } + issueManagement { + system 'github' + url 'https://github.com/Netflix/${rootProject.githubProjectName}/issues' + } + } } + group = 'com.netflix.osstemplate' // TEMPLATE: Set to organization of project + dependencies { compile 'javax.ws.rs:jsr311-api:1.1.1' compile 'com.sun.jersey:jersey-core:1.11' diff --git a/gradle/convention.gradle b/gradle/convention.gradle index a3fc06dd04..9255161209 100644 --- a/gradle/convention.gradle +++ b/gradle/convention.gradle @@ -26,7 +26,6 @@ subprojects } artifacts { - archives jar archives sourcesJar archives javadocJar } diff --git a/gradle/license.gradle b/gradle/license.gradle index 9d04830321..1fdc2702b4 100644 --- a/gradle/license.gradle +++ b/gradle/license.gradle @@ -1,5 +1,5 @@ buildscript { - dependencies { classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.4' } + dependencies { classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.5' } } -apply plugin: 'license' \ No newline at end of file +apply plugin: nl.javadude.gradle.plugins.license.LicensePlugin diff --git a/gradle/maven.gradle b/gradle/maven.gradle index cb75dfb637..ab2792ff33 100644 --- a/gradle/maven.gradle +++ b/gradle/maven.gradle @@ -4,14 +4,14 @@ subprojects { apply plugin: 'signing' signing { - required { performingRelease && gradle.taskGraph.hasTask("uploadMavenCentral")} + required { performingRelease && gradle.taskGraph.hasTask("uploadArchives")} sign configurations.archives } /** * Publishing to Maven Central example provided from http://jedicoder.blogspot.com/2011/11/automated-gradle-project-deployment-to.html */ - task uploadMavenCentral(type:Upload) { + task uploadArchives(type:Upload) { configuration = configurations.archives dependsOn signArchives doFirst { @@ -35,7 +35,6 @@ subprojects { artifactId 'oss-parent' version '7' } - url "https://github.com/Netflix/${rootProject.ext.githubProjectName}" licenses { license { name 'The Apache Software License, Version 2.0' @@ -43,15 +42,6 @@ subprojects { distribution 'repo' } } - scm { - connection "scm:git:git@github.com:Netflix/${rootProject.ext.githubProjectName}.git" - url "scm:git:git@github.com:Netflix/${rootProject.ext.githubProjectName}.git" - developerConnection "scm:git:git@github.com:Netflix/${rootProject.ext.githubProjectName}.git" - } - issueManagement { - system 'github' - url 'https://github.com/Netflix/${rootProject.ext.githubProjectName}/issues' - } } } } diff --git a/gradle/release.gradle b/gradle/release.gradle new file mode 100644 index 0000000000..8fc34dbff6 --- /dev/null +++ b/gradle/release.gradle @@ -0,0 +1,6 @@ +buildscript { + dependencies { classpath group: 'no.entitas.gradle', name: 'gradle-release-plugin', version: '1.11' } +} + +apply plugin: no.entitas.gradle.git.GitReleasePlugin // 'gitrelease' + From 9fa9ec0acce8afc01da943b1922a731059cb4cd2 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Mon, 9 Apr 2012 15:06:15 -0700 Subject: [PATCH 004/357] Avoid signatures in archives unless doing mavenCentral build --- build.gradle | 21 ++++++++++----------- gradle/maven.gradle | 14 +++++++++----- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/build.gradle b/build.gradle index c0d2d5e7ff..0fc71a5055 100644 --- a/build.gradle +++ b/build.gradle @@ -20,16 +20,16 @@ subprojects { // Closure to configure all the POM with extra info, common to all projects pom { project { - url "https://github.com/Netflix/${rootProject.githubProjectName}" - scm { - connection "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" - url "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" - developerConnection "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" - } - issueManagement { - system 'github' - url 'https://github.com/Netflix/${rootProject.githubProjectName}/issues' - } + url "https://github.com/Netflix/${rootProject.githubProjectName}" + scm { + connection "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" + url "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" + developerConnection "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" + } + issueManagement { + system 'github' + url 'https://github.com/Netflix/${rootProject.githubProjectName}/issues' + } } } @@ -57,7 +57,6 @@ project(':template-server') { compile 'com.sun.jersey:jersey-server:1.11' compile 'com.sun.jersey:jersey-servlet:1.11' compile project(':template-client') - testCompile 'org.mockito:mockito-core:1.8.5' } } diff --git a/gradle/maven.gradle b/gradle/maven.gradle index ab2792ff33..3de0990466 100644 --- a/gradle/maven.gradle +++ b/gradle/maven.gradle @@ -3,17 +3,21 @@ subprojects { apply plugin: 'maven' // Java plugin has to have been already applied for the conf2scope mappings to work apply plugin: 'signing' - signing { - required { performingRelease && gradle.taskGraph.hasTask("uploadArchives")} - sign configurations.archives + gradle.taskGraph.whenReady { taskGraph -> + if (taskGraph.hasTask("uploadMavenCentral")) { + signing { + required true + sign configurations.archives + } + } } /** * Publishing to Maven Central example provided from http://jedicoder.blogspot.com/2011/11/automated-gradle-project-deployment-to.html */ - task uploadArchives(type:Upload) { + task uploadMavenCentral(type:Upload) { configuration = configurations.archives - dependsOn signArchives + dependsOn 'signArchives' doFirst { repositories.mavenDeployer { beforeDeployment { org.gradle.api.artifacts.maven.MavenDeployment deployment -> signing.signPom(deployment) } From 3a10a077f9fc6c3aff7b7b1eebe9365d34b5acb4 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Mon, 9 Apr 2012 15:30:00 -0700 Subject: [PATCH 005/357] Remove local testing file --- gradle/local.gradle | 1 - 1 file changed, 1 deletion(-) delete mode 100644 gradle/local.gradle diff --git a/gradle/local.gradle b/gradle/local.gradle deleted file mode 100644 index 6f2d204b8a..0000000000 --- a/gradle/local.gradle +++ /dev/null @@ -1 +0,0 @@ -apply from: 'file://Users/jryan/Workspaces/jryan_build/Tools/nebula-boot/artifactory.gradle' From 61b1710621d138556fe2d5621076ea40ad47f8af Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Mon, 9 Apr 2012 15:56:00 -0700 Subject: [PATCH 006/357] Multimodule builds need a dump signing task --- gradle/maven.gradle | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/gradle/maven.gradle b/gradle/maven.gradle index 3de0990466..1673a24f8c 100644 --- a/gradle/maven.gradle +++ b/gradle/maven.gradle @@ -9,6 +9,10 @@ subprojects { required true sign configurations.archives } + } else { + task signArchives { + // do nothing + } } } @@ -17,7 +21,7 @@ subprojects { */ task uploadMavenCentral(type:Upload) { configuration = configurations.archives - dependsOn 'signArchives' + dependsOn { 'signArchives' } doFirst { repositories.mavenDeployer { beforeDeployment { org.gradle.api.artifacts.maven.MavenDeployment deployment -> signing.signPom(deployment) } From 66332d8b8fe98c8068c85c215dbbacc66397a68f Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Mon, 9 Apr 2012 16:00:55 -0700 Subject: [PATCH 007/357] Fix quotes --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0fc71a5055..55e5eeb557 100644 --- a/build.gradle +++ b/build.gradle @@ -28,7 +28,7 @@ subprojects { } issueManagement { system 'github' - url 'https://github.com/Netflix/${rootProject.githubProjectName}/issues' + url "https://github.com/Netflix/${rootProject.githubProjectName}/issues" } } } From 1df6e445488edfec78512c70e4db7352a1df57ec Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Mon, 9 Apr 2012 17:04:28 -0700 Subject: [PATCH 008/357] Use lifecycle to add signing task --- gradle/maven.gradle | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/gradle/maven.gradle b/gradle/maven.gradle index 1673a24f8c..560e66b4d4 100644 --- a/gradle/maven.gradle +++ b/gradle/maven.gradle @@ -3,16 +3,14 @@ subprojects { apply plugin: 'maven' // Java plugin has to have been already applied for the conf2scope mappings to work apply plugin: 'signing' - gradle.taskGraph.whenReady { taskGraph -> - if (taskGraph.hasTask("uploadMavenCentral")) { - signing { - required true - sign configurations.archives - } - } else { - task signArchives { - // do nothing - } + if (gradle.startParameter.taskNames.contains("uploadMavenCentral")) { + signing { + required true + sign configurations.archives + } + } else { + task signArchives { + // do nothing } } @@ -21,7 +19,7 @@ subprojects { */ task uploadMavenCentral(type:Upload) { configuration = configurations.archives - dependsOn { 'signArchives' } + dependsOn 'signArchives' doFirst { repositories.mavenDeployer { beforeDeployment { org.gradle.api.artifacts.maven.MavenDeployment deployment -> signing.signPom(deployment) } From 7c28a7637fbaf8c78ee8efcdae85592663960ea6 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Mon, 9 Apr 2012 19:10:02 -0700 Subject: [PATCH 009/357] Create branch that contains only build related files --- template-client/.classpath | 11 ---- template-client/.project | 19 ------- .../com.springsource.sts.gradle.core.prefs | 4 -- .../com.springsource.sts.gradle.refresh.prefs | 9 --- .../template/common/Conversation.class | Bin 214 -> 0 bytes .../netflix/template/client/TalkClient.java | 52 ------------------ .../netflix/template/common/Conversation.java | 21 ------- .../com/netflix/template/common/Sentence.java | 39 ------------- template-server/.classpath | 12 ---- template-server/.project | 19 ------- .../com.springsource.sts.gradle.core.prefs | 4 -- .../com.springsource.sts.gradle.refresh.prefs | 9 --- .../netflix/template/server/TalkServer.class | Bin 872 -> 0 bytes .../netflix/template/server/TalkServer.java | 26 --------- .../src/main/webapp/WEB-INF/web.xml | 25 --------- 15 files changed, 250 deletions(-) delete mode 100644 template-client/.classpath delete mode 100644 template-client/.project delete mode 100644 template-client/.settings/gradle/com.springsource.sts.gradle.core.prefs delete mode 100644 template-client/.settings/gradle/com.springsource.sts.gradle.refresh.prefs delete mode 100644 template-client/bin/com/netflix/template/common/Conversation.class delete mode 100644 template-client/src/main/java/com/netflix/template/client/TalkClient.java delete mode 100644 template-client/src/main/java/com/netflix/template/common/Conversation.java delete mode 100644 template-client/src/main/java/com/netflix/template/common/Sentence.java delete mode 100644 template-server/.classpath delete mode 100644 template-server/.project delete mode 100644 template-server/.settings/gradle/com.springsource.sts.gradle.core.prefs delete mode 100644 template-server/.settings/gradle/com.springsource.sts.gradle.refresh.prefs delete mode 100644 template-server/bin/com/netflix/template/server/TalkServer.class delete mode 100644 template-server/src/main/java/com/netflix/template/server/TalkServer.java delete mode 100644 template-server/src/main/webapp/WEB-INF/web.xml diff --git a/template-client/.classpath b/template-client/.classpath deleted file mode 100644 index cd1d475d6b..0000000000 --- a/template-client/.classpath +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/template-client/.project b/template-client/.project deleted file mode 100644 index 66279e9c19..0000000000 --- a/template-client/.project +++ /dev/null @@ -1,19 +0,0 @@ - - - template-client - - - - - - org.eclipse.jdt.core.javabuilder - - - - - - com.springsource.sts.gradle.core.nature - org.eclipse.jdt.core.javanature - org.eclipse.jdt.groovy.core.groovyNature - - diff --git a/template-client/.settings/gradle/com.springsource.sts.gradle.core.prefs b/template-client/.settings/gradle/com.springsource.sts.gradle.core.prefs deleted file mode 100644 index 1e7f022837..0000000000 --- a/template-client/.settings/gradle/com.springsource.sts.gradle.core.prefs +++ /dev/null @@ -1,4 +0,0 @@ -#com.springsource.sts.gradle.core.preferences.GradleProjectPreferences -#Sat Mar 17 22:40:30 PDT 2012 -com.springsource.sts.gradle.rootprojectloc=.. -com.springsource.sts.gradle.linkedresources= diff --git a/template-client/.settings/gradle/com.springsource.sts.gradle.refresh.prefs b/template-client/.settings/gradle/com.springsource.sts.gradle.refresh.prefs deleted file mode 100644 index 394fb107f1..0000000000 --- a/template-client/.settings/gradle/com.springsource.sts.gradle.refresh.prefs +++ /dev/null @@ -1,9 +0,0 @@ -#com.springsource.sts.gradle.core.actions.GradleRefreshPreferences -#Sat Mar 17 22:40:30 PDT 2012 -enableAfterTasks=true -afterTasks=afterEclipseImport; -useHierarchicalNames=false -enableBeforeTasks=true -addResourceFilters=true -enableDSLD=true -beforeTasks=cleanEclipse;eclipse; diff --git a/template-client/bin/com/netflix/template/common/Conversation.class b/template-client/bin/com/netflix/template/common/Conversation.class deleted file mode 100644 index 86d42f57590c9fbee4a60450fd6eca4121b4cf6a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 214 zcmaKmF%H5o6hoZ?ZRrHJC`%);G9ob{G4uo>`mIu>KPZI4*%&wghe9M96O$!dw%_~n zd;!>^Dv$}(+KrMabk;m%pz&f=AQ{ckvD`bJ$X``3jtk5MR)d<9w2FIqIuE3SK-qhu zV7QN4_2&3*t|bn{ns%|(DNlE@R-kI#&1*UsO9JcP%O<_$0s^y03}lgDfgFjXNE(we H`B;7deJVPB diff --git a/template-client/src/main/java/com/netflix/template/client/TalkClient.java b/template-client/src/main/java/com/netflix/template/client/TalkClient.java deleted file mode 100644 index fc9d20d33d..0000000000 --- a/template-client/src/main/java/com/netflix/template/client/TalkClient.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.netflix.template.client; - -import com.netflix.template.common.Conversation; -import com.netflix.template.common.Sentence; -import com.sun.jersey.api.client.Client; -import com.sun.jersey.api.client.WebResource; -import com.sun.jersey.api.client.filter.LoggingFilter; - -import javax.ws.rs.core.MediaType; - -/** - * Delegates to remote TalkServer over REST. - * @author jryan - * - */ -public class TalkClient implements Conversation { - - private WebResource webResource; - - /** - * Instantiate client. - * - * @param location URL to the base of resources, e.g. http://localhost:8080/template-server/rest - */ - public TalkClient(String location) { - Client client = Client.create(); - client.addFilter(new LoggingFilter(System.out)); - webResource = client.resource(location + "/talk"); - } - - @Override - public Sentence greeting() { - Sentence s = webResource.accept(MediaType.APPLICATION_XML).get(Sentence.class); - return s; - } - - @Override - public Sentence farewell() { - Sentence s = webResource.accept(MediaType.APPLICATION_XML).delete(Sentence.class); - return s; - } - - /** - * Tests out client. - * @param args Not applicable - */ - public static void main(String[] args) { - TalkClient remote = new TalkClient("http://localhost:8080/template-server/rest"); - System.out.println(remote.greeting().getWhole()); - System.out.println(remote.farewell().getWhole()); - } -} diff --git a/template-client/src/main/java/com/netflix/template/common/Conversation.java b/template-client/src/main/java/com/netflix/template/common/Conversation.java deleted file mode 100644 index c190f03bb7..0000000000 --- a/template-client/src/main/java/com/netflix/template/common/Conversation.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.netflix.template.common; - -/** - * Hold a conversation. - * @author jryan - * - */ -public interface Conversation { - - /** - * Initiates a conversation. - * @return Sentence words from geeting - */ - Sentence greeting(); - - /** - * End the conversation. - * @return - */ - Sentence farewell(); -} diff --git a/template-client/src/main/java/com/netflix/template/common/Sentence.java b/template-client/src/main/java/com/netflix/template/common/Sentence.java deleted file mode 100644 index 616f72efb0..0000000000 --- a/template-client/src/main/java/com/netflix/template/common/Sentence.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.netflix.template.common; - -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; - -/** - * Container for words going back and forth. - * @author jryan - * - */ -@XmlRootElement -public class Sentence { - private String whole; - - @SuppressWarnings("unused") - private Sentence() { - }; - - /** - * Initialize sentence. - * @param whole - */ - public Sentence(String whole) { - this.whole = whole; - } - - /** - * whole getter. - * @return - */ - @XmlElement - public String getWhole() { - return whole; - } - - public void setWhole(String whole) { - this.whole = whole; - } -} diff --git a/template-server/.classpath b/template-server/.classpath deleted file mode 100644 index 09505c5c72..0000000000 --- a/template-server/.classpath +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/template-server/.project b/template-server/.project deleted file mode 100644 index 0b2a3869fa..0000000000 --- a/template-server/.project +++ /dev/null @@ -1,19 +0,0 @@ - - - template-server - - - - - - org.eclipse.jdt.core.javabuilder - - - - - - com.springsource.sts.gradle.core.nature - org.eclipse.jdt.core.javanature - org.eclipse.jdt.groovy.core.groovyNature - - diff --git a/template-server/.settings/gradle/com.springsource.sts.gradle.core.prefs b/template-server/.settings/gradle/com.springsource.sts.gradle.core.prefs deleted file mode 100644 index 1e7f022837..0000000000 --- a/template-server/.settings/gradle/com.springsource.sts.gradle.core.prefs +++ /dev/null @@ -1,4 +0,0 @@ -#com.springsource.sts.gradle.core.preferences.GradleProjectPreferences -#Sat Mar 17 22:40:30 PDT 2012 -com.springsource.sts.gradle.rootprojectloc=.. -com.springsource.sts.gradle.linkedresources= diff --git a/template-server/.settings/gradle/com.springsource.sts.gradle.refresh.prefs b/template-server/.settings/gradle/com.springsource.sts.gradle.refresh.prefs deleted file mode 100644 index 394fb107f1..0000000000 --- a/template-server/.settings/gradle/com.springsource.sts.gradle.refresh.prefs +++ /dev/null @@ -1,9 +0,0 @@ -#com.springsource.sts.gradle.core.actions.GradleRefreshPreferences -#Sat Mar 17 22:40:30 PDT 2012 -enableAfterTasks=true -afterTasks=afterEclipseImport; -useHierarchicalNames=false -enableBeforeTasks=true -addResourceFilters=true -enableDSLD=true -beforeTasks=cleanEclipse;eclipse; diff --git a/template-server/bin/com/netflix/template/server/TalkServer.class b/template-server/bin/com/netflix/template/server/TalkServer.class deleted file mode 100644 index f534d0627d9bc6892160f0803ec67e0b1aa1bf62..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 872 zcmah{%Wl&^6g`tBv2jyEO`p6ArAk>a5@HvGgi0t23n~&tLaZjvG@UY@iR?)l{t8x= zK;i@VD8#i}K?0Uoc5jFUaVL|p7Eba^rc;^n zp3on=h3lcpaP3q~1=qri_}js$jGc!%L#q^lf{8W!z#0O|gj3cq)SoG%+;fJd)_$L% zdSHh#z!H`l@Zd8vBW2{9NivXWPYkqV2qPN{-506K@0Y=hn*hfZ!E-) zQahZ)GNT{0sn8RurYXi_t>OZM&l2rni($94ioewOxIr+lrPemUCT`^oyUnoPDkv{z z(se0S*v>oaAB$9;Q8vTcf~c3BsMG7Tee5uJht>`UpGa5GwUacKuTIZIBU^iPj^GP96*TCq7r_;*kl(mSz*RKq zMhk{j$_mL3$zCVBM$z>TU>PJaRaP9Q;PUvw(c}zsUDW Ykhe;ZE4W|qKPXf$liJ-}afXM#0MazXfdBvi diff --git a/template-server/src/main/java/com/netflix/template/server/TalkServer.java b/template-server/src/main/java/com/netflix/template/server/TalkServer.java deleted file mode 100644 index a856ce88a2..0000000000 --- a/template-server/src/main/java/com/netflix/template/server/TalkServer.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.netflix.template.server; - -import com.netflix.template.common.Conversation; -import com.netflix.template.common.Sentence; - -import javax.ws.rs.GET; -import javax.ws.rs.DELETE; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; - -@Path("/talk") -public class TalkServer implements Conversation { - - @GET - @Produces(MediaType.APPLICATION_XML) - public Sentence greeting() { - return new Sentence("Hello"); - } - - @DELETE - @Produces(MediaType.APPLICATION_XML) - public Sentence farewell() { - return new Sentence("Goodbye"); - } -} \ No newline at end of file diff --git a/template-server/src/main/webapp/WEB-INF/web.xml b/template-server/src/main/webapp/WEB-INF/web.xml deleted file mode 100644 index 273135a292..0000000000 --- a/template-server/src/main/webapp/WEB-INF/web.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - Jersey REST Service - - com.sun.jersey.spi.container.servlet.ServletContainer - - - com.sun.jersey.config.property.packages - com.netflix.template.server - - - com.sun.jersey.api.json.POJOMappingFeature - true - - 1 - - - - Jersey REST Service - /rest/* - - From bc662051d8c72ea7b20350b1746e1a8f527c9244 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Mon, 9 Apr 2012 19:14:47 -0700 Subject: [PATCH 010/357] Un-indenting HEADER --- codequality/HEADER | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/codequality/HEADER b/codequality/HEADER index b27b192925..6c5c7c9c77 100644 --- a/codequality/HEADER +++ b/codequality/HEADER @@ -1,13 +1,13 @@ - Copyright 2012 Netflix, Inc. +Copyright 2012 Netflix, Inc. - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. From bf5b268244d4eea430049627a734b3fa19307b3e Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Fri, 13 Apr 2012 15:40:46 -0700 Subject: [PATCH 011/357] Make one less thing people have to change --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 55e5eeb557..db70396359 100644 --- a/build.gradle +++ b/build.gradle @@ -33,7 +33,7 @@ subprojects { } } - group = 'com.netflix.osstemplate' // TEMPLATE: Set to organization of project + group = "com.netflix.${githubProjectName}" // TEMPLATE: Set to organization of project dependencies { compile 'javax.ws.rs:jsr311-api:1.1.1' From eaa8fc97a4427d51bf8637570a263d4f4e33e5af Mon Sep 17 00:00:00 2001 From: Jordan Zimmerman Date: Fri, 3 Aug 2012 11:59:35 -0700 Subject: [PATCH 012/357] Sonatype URL was wrong --- gradle/maven.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/maven.gradle b/gradle/maven.gradle index 560e66b4d4..7efb83333b 100644 --- a/gradle/maven.gradle +++ b/gradle/maven.gradle @@ -27,7 +27,7 @@ subprojects { // To test deployment locally, use the following instead of oss.sonatype.org //repository(url: "file://localhost/${rootProject.rootDir}/repo") - repository(url: 'http://oss.sonatype.org/services/local/staging/deply/maven2/') { + repository(url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2') { authentication(userName: rootProject.sonatypeUsername, password: rootProject.sonatypePassword) } From 0f0c6114889de1c0c3d702661aa44f95ec76edea Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Fri, 17 Aug 2012 16:02:17 -0700 Subject: [PATCH 013/357] Enable license header plugin --- build.gradle | 4 +++- codequality/HEADER | 2 +- gradle/buildscript.gradle | 3 +++ gradle/license.gradle | 12 ++++++++---- 4 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 gradle/buildscript.gradle diff --git a/build.gradle b/build.gradle index db70396359..fae039b42b 100644 --- a/build.gradle +++ b/build.gradle @@ -4,6 +4,7 @@ ext.githubProjectName = rootProject.name // TEMPLATE: change to match github pro buildscript { repositories { mavenCentral() } + apply from: file('gradle/buildscript.gradle'), to: buildscript } allprojects { @@ -14,7 +15,7 @@ allprojects { apply from: file('gradle/convention.gradle') apply from: file('gradle/maven.gradle') apply from: file('gradle/check.gradle') -//apply from: file('gradle/license.gradle') // Waiting for re-release +apply from: file('gradle/license.gradle') subprojects { // Closure to configure all the POM with extra info, common to all projects @@ -44,6 +45,7 @@ subprojects { } project(':template-client') { + apply plugin: 'java' dependencies { compile 'org.slf4j:slf4j-api:1.6.3' compile 'com.sun.jersey:jersey-client:1.11' diff --git a/codequality/HEADER b/codequality/HEADER index 6c5c7c9c77..3102e4b449 100644 --- a/codequality/HEADER +++ b/codequality/HEADER @@ -1,4 +1,4 @@ -Copyright 2012 Netflix, Inc. +Copyright ${year} Netflix, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/gradle/buildscript.gradle b/gradle/buildscript.gradle new file mode 100644 index 0000000000..77d13d1026 --- /dev/null +++ b/gradle/buildscript.gradle @@ -0,0 +1,3 @@ +// Executed in context of buildscript +dependencies { classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.6.0' } + diff --git a/gradle/license.gradle b/gradle/license.gradle index 1fdc2702b4..11a51f1137 100644 --- a/gradle/license.gradle +++ b/gradle/license.gradle @@ -1,5 +1,9 @@ -buildscript { - dependencies { classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.5' } -} +// Dependency for plugin was set in buildscript.gradle -apply plugin: nl.javadude.gradle.plugins.license.LicensePlugin +subprojects { +apply plugin: 'license' //nl.javadude.gradle.plugins.license.LicensePlugin +license { + header rootProject.file('codequality/HEADER') + ext.year = Calendar.getInstance().get(Calendar.YEAR) +} +} From c2af08e723518a2191e51bf9eae694fda12d1bbc Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Fri, 17 Aug 2012 16:02:31 -0700 Subject: [PATCH 014/357] Upgrade to Gradle 1.1. --- gradle/convention.gradle | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 39752 -> 45502 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 4 +- gradlew.bat | 180 +++++++++++------------ 5 files changed, 95 insertions(+), 95 deletions(-) diff --git a/gradle/convention.gradle b/gradle/convention.gradle index 9255161209..919e382901 100644 --- a/gradle/convention.gradle +++ b/gradle/convention.gradle @@ -40,5 +40,5 @@ task aggregateJavadoc(type: Javadoc) { // Generate wrapper, which is distributed as part of source to alleviate the need of installing gradle task createWrapper(type: Wrapper) { - gradleVersion = '1.0-milestone-9' + gradleVersion = '1.1' } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 2cb758a1c693a7cc7a489e4450ca1c700ac83049..7f1e239c8466c730b569575c494a89e6bb4c416a 100644 GIT binary patch delta 40518 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3798e7cf1c..9829a99a5b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Mar 14 13:19:56 PDT 2012 +#Tue Aug 14 16:28:54 PDT 2012 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=http\://services.gradle.org/distributions/gradle-1.0-milestone-9-bin.zip +distributionUrl=http\://services.gradle.org/distributions/gradle-1.1-bin.zip diff --git a/gradlew b/gradlew index ae91ed9029..e61422d06d 100755 --- a/gradlew +++ b/gradlew @@ -101,13 +101,13 @@ if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else - warn "Could not query businessSystem maximum file descriptor limit: $MAX_FD_LIMIT" + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then - JAVA_OPTS="$JAVA_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java diff --git a/gradlew.bat b/gradlew.bat index 8a0b282aa6..aec99730b4 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,90 +1,90 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega From a85e1963ffaacd0d695baa356f69adccfcbbebf4 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Mon, 20 Aug 2012 14:26:35 -0700 Subject: [PATCH 015/357] Adding cobertura --- gradle/buildscript.gradle | 6 ++++-- gradle/check.gradle | 7 +++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/gradle/buildscript.gradle b/gradle/buildscript.gradle index 77d13d1026..328acfc314 100644 --- a/gradle/buildscript.gradle +++ b/gradle/buildscript.gradle @@ -1,3 +1,5 @@ // Executed in context of buildscript -dependencies { classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.6.0' } - +dependencies { + classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.6.0' + classpath 'com.mapvine:gradle-cobertura-plugin:0.1' +} diff --git a/gradle/check.gradle b/gradle/check.gradle index 0f80516d45..7617f17b35 100644 --- a/gradle/check.gradle +++ b/gradle/check.gradle @@ -15,4 +15,11 @@ subprojects { apply plugin: 'pmd' //tasks.withType(Pmd) { reports.html.enabled true } + apply plugin: 'cobertura' + cobertura { + sourceDirs = sourceSets.main.java.srcDirs + format = 'html' + includes = ['**/*.java', '**/*.groovy'] + excludes = [] + } } From 8f289b73e539edc34a4fcd22df1c6dc9d97f216f Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Mon, 20 Aug 2012 23:34:49 -0700 Subject: [PATCH 016/357] Release plugin --- build.gradle | 5 ++- gradle.properties | 1 + gradle/buildscript.gradle | 7 +++++ gradle/convention.gradle | 11 +++---- gradle/maven.gradle | 15 +++------ gradle/release.gradle | 64 +++++++++++++++++++++++++++++++++++++-- 6 files changed, 79 insertions(+), 24 deletions(-) create mode 100644 gradle.properties diff --git a/build.gradle b/build.gradle index fae039b42b..65b498ec84 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,5 @@ // Establish version and status -ext.releaseVersion = '1.1.3' // TEMPLATE: Set to latest release -ext.githubProjectName = rootProject.name // TEMPLATE: change to match github project, if it doesn't match project name +ext.githubProjectName = rootProject.name // Change if github project name is not the same as the root project's name buildscript { repositories { mavenCentral() } @@ -11,11 +10,11 @@ allprojects { repositories { mavenCentral() } } -//apply from: file('gradle/release.gradle') // Not fully tested apply from: file('gradle/convention.gradle') apply from: file('gradle/maven.gradle') apply from: file('gradle/check.gradle') apply from: file('gradle/license.gradle') +apply from: file('gradle/release.gradle') subprojects { // Closure to configure all the POM with extra info, common to all projects diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000000..6b59bf6c43 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +version=1.4-SNAPSHOT diff --git a/gradle/buildscript.gradle b/gradle/buildscript.gradle index 328acfc314..59ffb3d33c 100644 --- a/gradle/buildscript.gradle +++ b/gradle/buildscript.gradle @@ -1,5 +1,12 @@ // Executed in context of buildscript +repositories { + ivy { + name = 'gradle_release' + artifactPattern 'http://launchpad.net/[organization]/trunk/[revision]/+download/[artifact]-[revision].jar' + } +} dependencies { classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.6.0' classpath 'com.mapvine:gradle-cobertura-plugin:0.1' + classpath 'gradle-release:gradle-release:1.0pre' } diff --git a/gradle/convention.gradle b/gradle/convention.gradle index 919e382901..65da4e30df 100644 --- a/gradle/convention.gradle +++ b/gradle/convention.gradle @@ -1,11 +1,8 @@ -ext.performingRelease = project.hasProperty('release') && Boolean.parseBoolean(project.release) -def versionPostfix = performingRelease?'':'-SNAPSHOT' -version = "${releaseVersion}${versionPostfix}" -status = performingRelease?'release':'snapshot' +// For Artifactory +rootProject.status = version.contains('-SNAPSHOT')?'snapshot':'release' -subprojects -{ +subprojects { project -> apply plugin: 'java' // Plugin as major conventions version = rootProject.version @@ -13,7 +10,7 @@ subprojects sourceCompatibility = 1.6 // GRADLE-2087 workaround, perform after java plugin - status = rootProject.status + status = version.contains('-SNAPSHOT')?'snapshot':'release' task sourcesJar(type: Jar, dependsOn:classes) { classifier = 'sources' diff --git a/gradle/maven.gradle b/gradle/maven.gradle index 7efb83333b..29f5d405d2 100644 --- a/gradle/maven.gradle +++ b/gradle/maven.gradle @@ -3,23 +3,16 @@ subprojects { apply plugin: 'maven' // Java plugin has to have been already applied for the conf2scope mappings to work apply plugin: 'signing' - if (gradle.startParameter.taskNames.contains("uploadMavenCentral")) { - signing { - required true - sign configurations.archives - } - } else { - task signArchives { - // do nothing - } + signing { + required { gradle.taskGraph.hasTask(uploadMavenCentral) } + sign configurations.archives } /** * Publishing to Maven Central example provided from http://jedicoder.blogspot.com/2011/11/automated-gradle-project-deployment-to.html */ - task uploadMavenCentral(type:Upload) { + task uploadMavenCentral(type:Upload, dependsOn: signArchives) { configuration = configurations.archives - dependsOn 'signArchives' doFirst { repositories.mavenDeployer { beforeDeployment { org.gradle.api.artifacts.maven.MavenDeployment deployment -> signing.signPom(deployment) } diff --git a/gradle/release.gradle b/gradle/release.gradle index 8fc34dbff6..fe4bc2ebdf 100644 --- a/gradle/release.gradle +++ b/gradle/release.gradle @@ -1,6 +1,64 @@ -buildscript { - dependencies { classpath group: 'no.entitas.gradle', name: 'gradle-release-plugin', version: '1.11' } + +apply plugin: 'release' + +// Ignore release plugin's task because it calls out via GradleBuild. This is a good place to put an email to send out +task release(overwrite: true, dependsOn: commitNewVersion) << { + // This is a good place to put an email to send out +} +commitNewVersion.dependsOn updateVersion +updateVersion.dependsOn createReleaseTag +createReleaseTag.dependsOn preTagCommit +def buildTasks = tasks.matching { it.name =~ /:build/ } +preTagCommit.dependsOn buildTasks +preTagCommit.dependsOn checkSnapshotDependencies +//checkSnapshotDependencies.dependsOn confirmReleaseVersion // Introduced in 1.0, forces readLine +//confirmReleaseVersion.dependsOn unSnapshotVersion +checkSnapshotDependencies.dependsOn unSnapshotVersion // Remove once above is fixed +unSnapshotVersion.dependsOn checkUpdateNeeded +checkUpdateNeeded.dependsOn checkCommitNeeded +checkCommitNeeded.dependsOn initScmPlugin + +// Call out to compile against internal repository +task uploadArtifactory(type: GradleBuild) { + startParameter = project.gradle.startParameter.newInstance() + startParameter.addInitScript( file('gradle/netflix-oss.gradle') ) + startParameter.getExcludedTaskNames().add('check') + tasks = [ 'build', 'artifactoryPublish' ] } -apply plugin: no.entitas.gradle.git.GitReleasePlugin // 'gitrelease' +task buildWithArtifactory(type: GradleBuild) { + startParameter = project.gradle.startParameter.newInstance() + startParameter.addInitScript( file('gradle/netflix-oss.gradle') ) + startParameter.getExcludedTaskNames().add('check') + tasks = [ 'build' ] +} +// Ensure upload happens before taggging but after all pre-checks +uploadArtifactory.dependsOn checkSnapshotDependencies +createReleaseTag.dependsOn uploadArtifactory +gradle.taskGraph.whenReady { taskGraph -> + if ( taskGraph.hasTask(uploadArtifactory) && !taskGraph.hasTask(':release') ) { + throw new GradleException('"release" task has to be run before uploading to Artifactory') + } +} +subprojects.each { project -> + project.uploadMavenCentral.dependsOn rootProject.checkSnapshotDependencies + rootProject.createReleaseTag.dependsOn project.uploadMavenCentral + + gradle.taskGraph.whenReady { taskGraph -> + if ( taskGraph.hasTask(project.uploadMavenCentral) && !taskGraph.hasTask(':release') ) { + throw new GradleException('"release" task has to be run before uploading to Maven Central') + } + } +} + +// Prevent plugin from asking for a version number interactively +ext.'gradle.release.useAutomaticVersion' = "true" + +release { + // http://tellurianring.com/wiki/gradle/release + failOnCommitNeeded=false + failOnPublishNeeded=false + failOnUnversionedFiles=false + failOnUpdateNeeded=false +} From ddedbd72afbfbd786614983f3ffbc10e1522c6ec Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Fri, 31 Aug 2012 15:35:27 -0700 Subject: [PATCH 017/357] Pointing to a repo in our control --- gradle/buildscript.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/buildscript.gradle b/gradle/buildscript.gradle index 59ffb3d33c..d12c78383a 100644 --- a/gradle/buildscript.gradle +++ b/gradle/buildscript.gradle @@ -1,8 +1,8 @@ // Executed in context of buildscript repositories { - ivy { - name = 'gradle_release' - artifactPattern 'http://launchpad.net/[organization]/trunk/[revision]/+download/[artifact]-[revision].jar' + maven { + name 'build-repo' + url 'https://github.com/Netflix-Skunkworks/build-repo/raw/master/releases/' } } dependencies { From f170238c6df0e5991758ea14058e1b6ef05fa905 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Tue, 4 Sep 2012 11:46:58 -0700 Subject: [PATCH 018/357] Using custom build of release plugin, to support building from a branch --- gradle/buildscript.gradle | 2 +- gradle/release.gradle | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/gradle/buildscript.gradle b/gradle/buildscript.gradle index d12c78383a..c63c13006f 100644 --- a/gradle/buildscript.gradle +++ b/gradle/buildscript.gradle @@ -8,5 +8,5 @@ repositories { dependencies { classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.6.0' classpath 'com.mapvine:gradle-cobertura-plugin:0.1' - classpath 'gradle-release:gradle-release:1.0pre' + classpath 'gradle-release:gradle-release:1.0-SNAPSHOT' } diff --git a/gradle/release.gradle b/gradle/release.gradle index fe4bc2ebdf..8ed0305107 100644 --- a/gradle/release.gradle +++ b/gradle/release.gradle @@ -57,8 +57,9 @@ ext.'gradle.release.useAutomaticVersion' = "true" release { // http://tellurianring.com/wiki/gradle/release - failOnCommitNeeded=false - failOnPublishNeeded=false - failOnUnversionedFiles=false - failOnUpdateNeeded=false + failOnCommitNeeded=true + failOnPublishNeeded=true + failOnUnversionedFiles=true + failOnUpdateNeeded=true + requireBranch = null } From 1954d730193a6ee7300cbcdf5f4cfaa74e9faa3e Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Wed, 5 Sep 2012 16:33:00 -0700 Subject: [PATCH 019/357] Setting default name for multi-project --- settings.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/settings.gradle b/settings.gradle index 350f2f1b49..5dd25eb8c6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,2 @@ +rootProject.name='gradle-template-multi' // TEMPLATE: Change this include 'template-client','template-server' From 2b31d36a03edcc93a3d2c478ba8b8c3b315df85a Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Thu, 6 Sep 2012 15:27:23 -0700 Subject: [PATCH 020/357] Changes needed for release plugin --- .gitignore | 4 ++-- gradle/convention.gradle | 6 +++++- gradle/maven.gradle | 5 ----- gradle/release.gradle | 12 +++++++----- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 313af3cb82..79ab4710fa 100644 --- a/.gitignore +++ b/.gitignore @@ -44,8 +44,8 @@ Thumbs.db */target /build */build -# -# # IntelliJ specific files/directories + +# IntelliJ specific files/directories out .idea *.ipr diff --git a/gradle/convention.gradle b/gradle/convention.gradle index 65da4e30df..ce2701a8c5 100644 --- a/gradle/convention.gradle +++ b/gradle/convention.gradle @@ -10,7 +10,7 @@ subprojects { project -> sourceCompatibility = 1.6 // GRADLE-2087 workaround, perform after java plugin - status = version.contains('-SNAPSHOT')?'snapshot':'release' + status = rootProject.status task sourcesJar(type: Jar, dependsOn:classes) { classifier = 'sources' @@ -22,6 +22,10 @@ subprojects { project -> from javadoc.destinationDir } + // Ensure output is on a new line + javadoc.doFirst { println "" } + + artifacts { archives sourcesJar archives javadocJar diff --git a/gradle/maven.gradle b/gradle/maven.gradle index 29f5d405d2..581896b151 100644 --- a/gradle/maven.gradle +++ b/gradle/maven.gradle @@ -29,11 +29,6 @@ subprojects { // Closure to configure all the POM with extra info, common to all projects pom.project { - parent { - groupId 'org.sonatype.oss' - artifactId 'oss-parent' - version '7' - } licenses { license { name 'The Apache Software License, Version 2.0' diff --git a/gradle/release.gradle b/gradle/release.gradle index 8ed0305107..a7f9913d08 100644 --- a/gradle/release.gradle +++ b/gradle/release.gradle @@ -1,4 +1,3 @@ - apply plugin: 'release' // Ignore release plugin's task because it calls out via GradleBuild. This is a good place to put an email to send out @@ -25,6 +24,8 @@ task uploadArtifactory(type: GradleBuild) { startParameter.getExcludedTaskNames().add('check') tasks = [ 'build', 'artifactoryPublish' ] } +task releaseArtifactory(dependsOn: [checkSnapshotDependencies, uploadArtifactory]) + task buildWithArtifactory(type: GradleBuild) { startParameter = project.gradle.startParameter.newInstance() @@ -34,11 +35,11 @@ task buildWithArtifactory(type: GradleBuild) { } // Ensure upload happens before taggging but after all pre-checks -uploadArtifactory.dependsOn checkSnapshotDependencies -createReleaseTag.dependsOn uploadArtifactory +releaseArtifactory.dependsOn checkSnapshotDependencies +createReleaseTag.dependsOn releaseArtifactory gradle.taskGraph.whenReady { taskGraph -> - if ( taskGraph.hasTask(uploadArtifactory) && !taskGraph.hasTask(':release') ) { - throw new GradleException('"release" task has to be run before uploading to Artifactory') + if ( taskGraph.hasTask(uploadArtifactory) && rootProject.status == 'release' && !taskGraph.hasTask(':release') ) { + throw new GradleException('"release" task has to be run before uploading a release to Artifactory') } } subprojects.each { project -> @@ -61,5 +62,6 @@ release { failOnPublishNeeded=true failOnUnversionedFiles=true failOnUpdateNeeded=true + includeProjectNameInTag=true requireBranch = null } From a8674db7d92f5a6669b8f373205cfef52b995ba9 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Mon, 24 Sep 2012 14:53:11 -0700 Subject: [PATCH 021/357] Stop relying on maven convention on project --- build.gradle | 16 ---------------- gradle/maven.gradle | 10 ++++++++++ 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/build.gradle b/build.gradle index 65b498ec84..7f8fb72deb 100644 --- a/build.gradle +++ b/build.gradle @@ -17,22 +17,6 @@ apply from: file('gradle/license.gradle') apply from: file('gradle/release.gradle') subprojects { - // Closure to configure all the POM with extra info, common to all projects - pom { - project { - url "https://github.com/Netflix/${rootProject.githubProjectName}" - scm { - connection "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" - url "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" - developerConnection "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" - } - issueManagement { - system 'github' - url "https://github.com/Netflix/${rootProject.githubProjectName}/issues" - } - } - } - group = "com.netflix.${githubProjectName}" // TEMPLATE: Set to organization of project dependencies { diff --git a/gradle/maven.gradle b/gradle/maven.gradle index 581896b151..1b1e818fd2 100644 --- a/gradle/maven.gradle +++ b/gradle/maven.gradle @@ -36,6 +36,16 @@ subprojects { distribution 'repo' } } + url "https://github.com/Netflix/${rootProject.githubProjectName}" + scm { + connection "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" + url "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" + developerConnection "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" + } + issueManagement { + system 'github' + url "https://github.com/Netflix/${rootProject.githubProjectName}/issues" + } } } } From 6d4a854dca49cbfd989b468efc4e7bce89796e08 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Thu, 4 Oct 2012 17:47:56 -0700 Subject: [PATCH 022/357] Filling in more pom fields for Sonatype --- gradle/maven.gradle | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/gradle/maven.gradle b/gradle/maven.gradle index 1b1e818fd2..a3a3d44240 100644 --- a/gradle/maven.gradle +++ b/gradle/maven.gradle @@ -29,6 +29,15 @@ subprojects { // Closure to configure all the POM with extra info, common to all projects pom.project { + name "${project.name}" + description "${project.name} developed by Netflix" + developers { + developer { + id 'netflixgithub' + name 'Netflix Open Source Development' + email 'talent@netflix.com' + } + } licenses { license { name 'The Apache Software License, Version 2.0' From 61bd2b059be16052e2372e792ec8698af9589d79 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Wed, 10 Oct 2012 20:34:02 -0700 Subject: [PATCH 023/357] Add local publishing --- .gitignore | 1 + gradle/release.gradle | 26 ++++++++++++-------------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 79ab4710fa..c82b5347c5 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ Thumbs.db # Editor Files # ################ *~ +*.swp # Gradle Files # ################ diff --git a/gradle/release.gradle b/gradle/release.gradle index a7f9913d08..cd135643bd 100644 --- a/gradle/release.gradle +++ b/gradle/release.gradle @@ -17,23 +17,21 @@ unSnapshotVersion.dependsOn checkUpdateNeeded checkUpdateNeeded.dependsOn checkCommitNeeded checkCommitNeeded.dependsOn initScmPlugin -// Call out to compile against internal repository -task uploadArtifactory(type: GradleBuild) { - startParameter = project.gradle.startParameter.newInstance() - startParameter.addInitScript( file('gradle/netflix-oss.gradle') ) - startParameter.getExcludedTaskNames().add('check') - tasks = [ 'build', 'artifactoryPublish' ] +[ + uploadIvyLocal: 'uploadLocal', + uploadArtifactory: 'artifactoryPublish', // Call out to compile against internal repository + buildWithArtifactory: 'build' // Build against internal repository +].each { key, value -> + // Call out to compile against internal repository + task "${key}"(type: GradleBuild) { + startParameter = project.gradle.startParameter.newInstance() + startParameter.addInitScript( file('gradle/netflix-oss.gradle') ) + startParameter.getExcludedTaskNames().add('check') + tasks = [ 'build', value ] + } } task releaseArtifactory(dependsOn: [checkSnapshotDependencies, uploadArtifactory]) - -task buildWithArtifactory(type: GradleBuild) { - startParameter = project.gradle.startParameter.newInstance() - startParameter.addInitScript( file('gradle/netflix-oss.gradle') ) - startParameter.getExcludedTaskNames().add('check') - tasks = [ 'build' ] -} - // Ensure upload happens before taggging but after all pre-checks releaseArtifactory.dependsOn checkSnapshotDependencies createReleaseTag.dependsOn releaseArtifactory From 05c4d0660d4f5317e430f0dc7f0c1487ac4f51e2 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Mon, 22 Oct 2012 20:49:26 -0700 Subject: [PATCH 024/357] Putting javadoc and sources into proper confs and setting types --- gradle/convention.gradle | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/gradle/convention.gradle b/gradle/convention.gradle index ce2701a8c5..f16047e2f4 100644 --- a/gradle/convention.gradle +++ b/gradle/convention.gradle @@ -13,23 +13,30 @@ subprojects { project -> status = rootProject.status task sourcesJar(type: Jar, dependsOn:classes) { - classifier = 'sources' from sourceSets.main.allSource } task javadocJar(type: Jar, dependsOn:javadoc) { - classifier = 'javadoc' from javadoc.destinationDir } - // Ensure output is on a new line - javadoc.doFirst { println "" } - + configurations.add('sources') + configurations.add('javadoc') artifacts { - archives sourcesJar - archives javadocJar + sources(sourcesJar) { + type 'source' + classifier 'sources' + } + javadoc(javadocJar) { + type 'javadoc' + classifier 'javadoc' + } } + + // Ensure output is on a new line + javadoc.doFirst { println "" } + } task aggregateJavadoc(type: Javadoc) { From 1cbb4d6dbe34998f4caa2cbe52c59abc8e0e4886 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Tue, 23 Oct 2012 16:05:35 -0700 Subject: [PATCH 025/357] Fixing issue when publishing source/javadoc to maven central --- gradle/convention.gradle | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/gradle/convention.gradle b/gradle/convention.gradle index f16047e2f4..8e71812ecc 100644 --- a/gradle/convention.gradle +++ b/gradle/convention.gradle @@ -14,23 +14,40 @@ subprojects { project -> task sourcesJar(type: Jar, dependsOn:classes) { from sourceSets.main.allSource + classifier 'sources' + extension 'jar' } task javadocJar(type: Jar, dependsOn:javadoc) { from javadoc.destinationDir + classifier 'javadoc' + extension 'jar' } configurations.add('sources') configurations.add('javadoc') + configurations.archives { + extendsFrom configurations.sources + extendsFrom configurations.javadoc + } + + // When outputing to an Ivy repo, we want to use the proper type field + gradle.taskGraph.whenReady { + def isNotMaven = !it.hasTask(project.uploadMavenCentral) + if (isNotMaven) { + def artifacts = project.configurations.sources.artifacts + def sourceArtifact = artifacts.iterator().next() + sourceArtifact.type = 'sources' + } + } artifacts { sources(sourcesJar) { - type 'source' - classifier 'sources' + // Weird Gradle quirk where type will be used for the extension, but only for sources + type 'jar' } javadoc(javadocJar) { type 'javadoc' - classifier 'javadoc' } } From 66ff83f789785e9351cec4fb54b6f6ffa0eb9874 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Fri, 16 Nov 2012 14:46:32 -0800 Subject: [PATCH 026/357] Using a better github location --- gradle/buildscript.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gradle/buildscript.gradle b/gradle/buildscript.gradle index c63c13006f..4d6a29aabe 100644 --- a/gradle/buildscript.gradle +++ b/gradle/buildscript.gradle @@ -1,8 +1,9 @@ // Executed in context of buildscript repositories { + // Repo in addition to maven central maven { name 'build-repo' - url 'https://github.com/Netflix-Skunkworks/build-repo/raw/master/releases/' + url 'https://raw.github.com/Netflix-Skunkworks/build-repo/master/releases/' // gradle-release/gradle-release/1.0-SNAPSHOT/gradle-release-1.0-SNAPSHOT.jar } } dependencies { From 36e5b8f5d64e08147ea98ae17e15a21d716b8b10 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Fri, 14 Dec 2012 11:09:19 -0800 Subject: [PATCH 027/357] Adding provided scope Conflicts: gradle/convention.gradle --- gradle/convention.gradle | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/gradle/convention.gradle b/gradle/convention.gradle index 8e71812ecc..6122c8e878 100644 --- a/gradle/convention.gradle +++ b/gradle/convention.gradle @@ -54,6 +54,20 @@ subprojects { project -> // Ensure output is on a new line javadoc.doFirst { println "" } + configurations { + provided { + description = 'much like compile, but indicates you expect the JDK or a container to provide it. It is only available on the compilation classpath, and is not transitive.' + transitive = false + visible = false + } + } + + project.sourceSets { + main.compileClasspath += project.configurations.provided + main.runtimeClasspath -= project.configurations.provided + test.compileClasspath += project.configurations.provided + test.runtimeClasspath += project.configurations.provided + } } task aggregateJavadoc(type: Javadoc) { From b7847eb946161b85609621f0392eb33e32b5cf42 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Tue, 1 Jan 2013 21:12:24 -0800 Subject: [PATCH 028/357] Fixing transitive-ness of provided --- gradle/convention.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/convention.gradle b/gradle/convention.gradle index 6122c8e878..8b877071d9 100644 --- a/gradle/convention.gradle +++ b/gradle/convention.gradle @@ -57,8 +57,8 @@ subprojects { project -> configurations { provided { description = 'much like compile, but indicates you expect the JDK or a container to provide it. It is only available on the compilation classpath, and is not transitive.' - transitive = false - visible = false + transitive = true + visible = true } } From f5a10c63c56b2cc5c07c7a602be6cd965a8415d3 Mon Sep 17 00:00:00 2001 From: Greg Orzell Date: Thu, 7 Feb 2013 18:35:14 -0800 Subject: [PATCH 029/357] Update codequality/checkstyle.xml Removing as it appears to no longer be available in the latest version of checkstyle that comes with gradle 1.4 --- codequality/checkstyle.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/codequality/checkstyle.xml b/codequality/checkstyle.xml index 481d2829fd..47c01a2ea1 100644 --- a/codequality/checkstyle.xml +++ b/codequality/checkstyle.xml @@ -128,7 +128,6 @@ - From 6f9e3a024fd9213446079e6160ef872849e91c53 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Tue, 5 Mar 2013 10:20:24 -0800 Subject: [PATCH 030/357] Upgrading to Gradle 1.4 --- gradle/convention.gradle | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 45502 -> 46735 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- gradlew | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/gradle/convention.gradle b/gradle/convention.gradle index 8b877071d9..4f07d1a64b 100644 --- a/gradle/convention.gradle +++ b/gradle/convention.gradle @@ -79,5 +79,5 @@ task aggregateJavadoc(type: Javadoc) { // Generate wrapper, which is distributed as part of source to alleviate the need of installing gradle task createWrapper(type: Wrapper) { - gradleVersion = '1.1' + gradleVersion = '1.4' } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f1e239c8466c730b569575c494a89e6bb4c416a..42d9b0e9c5872910311a1d035995ab8ec466e7ac 100644 GIT binary patch delta 9381 zcmZ8n1zeO%*GE#3ZkCc}=?)2{rMp8qB%~Xb5|D-^r39All192aMY=&yIwigZyzl+I z&oaLm{(EBP%$f7d%-NPT*r=bdXo@m$@W?PQPoKghQwxa1pi!gV*ZXWjqk=FnFsd=H z**T8}nR=y$9f8AVMp7Q&$lcv1MHfR@VYt=^Pkt;T){4j)*P~$n!iD#P< z(!o}4vy3Ksl#K88E|L5f4mtr=O*u6|!Vmoy`sa}Vk(L$I3+qCv z>2_OZq$>fkBNpM{O*w7KYEE7c2+S8CtsVLS1+So+SPLbLB&Q*#ROo2#%^SFL#<=Bi zlIthKb%0qm#V>Yb=uW}Gh@@D$`;zlhT?tq;W(%QMdMjkx!UZwq z3B_bV@T(cRzKWtKm{HwQa`BT;V$v5o*A+|mNr|-sNv8>pIJAkUPn^qAL5{M;V{grb zLH3;|CF7LZS!pr}`Z#!B6fl0v%A;s$gF6i1@GWa3o|W5wu!iMZ?^&l8=SsK>y5Q%x z4@Ka&B+7f^-Sz=HDI>SPIfE{dES{1zz-rAwp|87J`UML`LJ_l!2R4A9gX7?lVvHPp?~@L*u*?*)5v5`vK2 zh&Bj)?lcekwyM^8<*m)N0;v)pGQKYE~zP}|?P ztID~2O8@Q<5uvugA~yAX8xd}klsJbxgIiUodi}TX+TJl8(_?Nv??!)Dy1IoZ*TSc( zdi0AC=e=W|rpH3FO-oVe7xofeBy}poz!PR%vd&=Db++s*U-Y6nqAFyetYr`>?woyD zXjR=vR8&6)+40&MvJiQ2MqJAIzz`Bu_V1gEIFl30SEQ;3gat>IvtH9tDJFM(uY}50 zpS!(j!@s$R6}UEi)j|=`SyI-2YsEEryNleCX+)5BbCJ9Eb)%=&^p3wU;;z1rmA?;hXNN1?0a8h`8@O`#4l<%zWu*sSrHev*xRd6#=lpxlTxhC9fyAUnue z@zp0zDm0Em=0kS)dbIIjl{XAcM_@;qcabKBgu(w5QeA(8!o4wwWHoRCDQ;~6^+7k<6 z)0^J^UE&=?>^1za!_>4l5}Px9X5vb3_N6Ejs^roa*K_7GKd55q?e`_cRA6%o+d@T30hh?2i*F;8b5r_?zhN@{yL~&090YsA|{n~3t1ik-1>ig zcafrcx#A1doej;NG3O38eS?!M<1qR{^P>wH zSRstfkW)7u0;Zp1qmuY92gWb6N23al9Id|8n$PC358KGTV4InJKl zXxSc;&c1h4_#Nv9?@YYb&`Z2{wHFwLb!{f7W$cbF?z2SX)t8mEhZXzk{buXLg^VY* zW~UnS7^Wao4(Tf=-4+$M_l!Hp@6nkykipW5h>6>zd5F0aF3>yom( z+3)*R^(S zac?QRYDaF(_B`o?_ow%%1ILV_6Vl)EDB?LyzO0M^bw6wb2mYqO)T|BJy=K0wNwtdN zdBKMYO3*jUsZH~=Lj9HA_%6&bDBb3WTSpA|)}Co&nxEGunRb4awIW(7V>hGXTQzyJ zvdw(-1h{&paCDV!*C40N%vpNpWLO@3pZJV-Z6jTc)I1GqMJ!bCXM>f~m}6zf?mS<5 zlF&KVG}h64;`qC*uv?(dR9ecp$Uy|@_4Y?i(0H8qi|cLS#!<{EF#|}H<7fU29fP@8 zMV$SYmyxw{uFtj(Q#_G;>8tyRVzbp{%kg ztVnX@X=YkB#t%#h$|?O|8!-Z5B<5BEW|#6`&WFo%Mtj}_D;mDS*IcUC1d;=89%NsU zPyof4O0tx~g=h|qXfrB35J%}~iFh((agd-Rq||6hD9o(giZE52;Ww9Pn$EH2z+D5h z?r<+2G4AlT;8*c)`NSxEcVSNmD90`;e{NMi=Lc}o!ppoGL&PQwwSCQD>no0?6ξ zcdZP(+GXXFc6A!&&yZn+TJ>(dhTL~wvx~{t9PX%B7S;Kco;1HmVIA|MNF8VjQ6=g zkBORFvG0Ic`LQ+TU@76A&|2?1kO7shqFALLyRTuX-p~}Bg%Ny74;>R{_cwqmMk$RA zHsy~#x6rjxJvaLF&ARBfKH|mb9HEe1+7PaC+Nd7XK(g0q0p?}TR~-;XaRbuCuzsTG zCQF`=PVw-~=`_y`?;Z8EOu4$W&Nha+H{3DS*Rq9YMaDFy>3e+10NE3nGI(la zJhxO}owW)njAR%ZT;(=9pxLye6=i8(o|BT<&)xc+{2Xo&uE016>t(@?+@w39g5)8J z0C?9%*BfsK9%yChXU;FwP`^$IW}~rb5FFKbefRpp$0D*hrveTR0Yx4yj-{_Hjle;d z4IjlS6^DD)toI7Bp)Koqr8px@y|S+=cK48c&G8yvjxH~IVRdXzp;1Tw3S|ecQuk0 z1rxrS@e+}htQ!Mwy*%Hs*OIqrjrfQj`K78R=%qD|a}&HIS}CyrP;=a#6kkzIuURXVk%@^J7$O|Vr^Npqs|w`E-No~F z=7ZP8JL~H$A}DTQd}fB8<1Tos&m)tE!Y&YkGoRA>Otu(o*l-LilMa5H_Oi`f4;k7y z?63}(8qxle_1Qp@T35#HFnLFedNP&dRl>mYSF%%sW+W@)uE!Eeo2(4FEaqy~JfQcJ zYM&MK2Sc(<8t505MjJM{4c*%-1_joDe3x=T;7M!d`gXp`7xd9=&5k*j?!vxA@=+XA zMXE`7?AHld#w@+tG<&g}(ybX;YK`l_ZQZCXw?2jhfn5D01Z{Ly$#B;>#Ij;c855K8 zIE&txug-d}45tbupUUUP@d&b2;em4eBQ-(I+GEc~JtDT14b(NYvkQjm*E`U?}{ zewL~mesi(J#p$x#dOZT_kP;%)d>0^W!bHuNLb4k2XvQldIPQADBEw`%j8 zcF#@WI5ds4!t?8FmfzQ98_l*N3vnJ`{vx>0-0Vg&UiU%(WLFkJnoON_@|9KRNGiN6 zoJzT)<;YTh>DhWE&SB4Oke-0YA=!HW6c*H+U)$6c%l z?Ar~2e^2RzKQy2?(T`O<_f+GC>o>Qns3=*<$#0)Gt% zZ|YtKa@R&zWyu1KL2Yg9pL#?FETmYnx#ae3WI|-cMNO-t5RxV$*|-29))9rKP7-jY zP7w%mSK1oYY)nlGQHa^vgmdGb9jN<83VAJD*P-i&ontAdVlEYJHIavZZA$|GL5V zzzfh+h6aaOX3CoWuQL@R6uIJ5+i7{05z_2VI>*`!KF@FrnP>%!gstQhlq%QAOULB; zfx+n_H$)BA4?J{-cmwdxK_EX^oi>QW*T*Ovs*A;25qb`BYA{Ms#VMee!X(x z9gQTI$7GoX0FDXPDjN}VaYzl_Tjj+I*-KDi9jDDxq@tna8eb~(4)JrMtN|$Rv>MQp zgd?i#D00S07b$VYPX{UQOpmf6O^f{CMl37KYn*Wfxy43K$hHBi*-~v#ERghB!yy3={Xtuw+&b}Z_-DFR(^d*&uAX_N7r~T-t8Bq+xMFwnm5NkfJZ+2J zv3WfmCAvJ}Q)SWmvnP}H_3f*_U&lsic+cgbw@3Gd5?ah148i2$oS?Iz5tRe! zK^4|YMOM}d_9qlF2<9^w2$XQ5#-;vQ*Ts|Tu|{(zS)FmIstfC>vZYz$O|gUXsyfX7 z?_M($dXZGmVT9idmHQi72%yEt+0xpC$-&gv)Y8t0+0Nm;ovDM9rKuxVM6+$@3yeTd zed?E;88#r@QAik$9+=LiI!TZ$#t%EJM8Kzxif3VPJLS-GzTH#y4eIz1kh~_kAK503 zFhdMqM_Rg~86K%az|t6rm?psGkk=#1P~8!RLz5u_3BiJSPdnfA&Gxl-+J!c`b+3$` z7Ni!1ry*gJxg4{Sg?AZ_p({3R9UDJGq(Wqi|5gxie7i7|t>Lx)J$>Dcq8LeCJ@GhQ zK)vTI{aXJEg8HCb)f&17V!Mzv=`GoIANs#Wy&p9Avp56%eWPkboE`3=1xAqCeuNIC z&S4*0d$K?Nw7x6EJbA?4E6pK2w&b;_9$Bkgba~-0@5M=7hjm@S!@z)_!@$shN8?Gr z^KSvTs;x4(s;pE-)+LF^Q}m%(y2Y7_IpF68bRZ5(?>EDIOP9k!o}eWmhRe16w;;F! z=9VzyS7vI1U1NT;%zGVP__iy?e2ru{jK=dw zn}QY!W<}a!HWxl#jZY5b=q^@7I9Oi8Dp)k=6_$LgtU9P}7v=ZX0_WSH;3pZ)&05z?wJnDPHv+}=XeIB@`USQ_X54v%%yMfeV2o zk@kF6-RDSCutld9m*!{(M}l5|ymhlhsBRml1K9cKDAyf7H-{KiANlGNi#eAdtl`V= zoRq((>nqU?W9>F;o60!^0d^gBR>ea#AJv*Y`l|f=vjtA^Oh>(f-9S8j>)s2C&CVd# zD#}Zd_reaUsk~-qnmAn{8rj)VAfyvtWH-XHNEzoi?A%=IJWl_L0T#W)NHq2eOsQN z4f=YfYn;Tb{PZo821ai>UIeKYTheual$FK~a7!)Ah3Wg$>*?LJ= zQYL_d&{J}?if8I-JjSj$-`k$1qcNMm%%Ft9(R4m|#7($#KCIG4=PgDy_>r~=#HH*K zY_>lQJhXG2(4mlQru&_Swkh2sb}B9Y#xe54Rbw+#b}n6Fs#-1L1Tq?g_KtynQ}Yxf z=)$-n0 zUGmI!-7#|7ZitCbccimeRf-}mXpnSCOty_LYAojr|dgvH4=FEHQAq z)821iyQu=u#mx?0H^@S+^Z#F zBClbFzlxJFix}FtCsDw(2Df|EiX$87Z(;a?_G#>Mgv)3IWmty>osCrFnC$YGWbVN% z>^k}ov&!9Vw0TfM@Nhkb_i436pPH_b==xA1k?ieBYp2!8D4?0cB+d(tiJ=E|Pj@xR zBf!Z8t}&f5T@KCG;9Qxz1|<2b4-&^Ob?RP!Gr~IhiGMlQ+F+l1>iAj#UY)Bu>m+s% zW{QNO8B>|41Giz7-ZhuEF)}5o+h%_GRXL$RYpTMh14x%0I(RUCyfkPANSHriGYwqI zm6vRKDYxXGZv8T#lIvP(exT}+>@}$bonXKnseWS7megZRDvjVLos#7&c496 z?l;c)=ZHE?h53r0JOxk4q^t?a&69G9w!o;U@5_#qlVb(yl0=!2EjwmI*Y{bDKS_>< zAbMbO@h3X&nQ{J!j#Rdzf3S=9<$t1RvBM0;W9$qLIQoNm2d)2UnSMk>c!;9lv?kmp zSQwZW2rw{=U^ZK9a8x86h$2T9N0<&jlc_%xze%wOo{ikH6M`7A(Xy5d1CPo^29iRRDI)<<3>y?nzmQW;QEYCq}dkx`SQ02f`(&ABIfkxmi z6T@}^a)*)rzzVWigHNJ=RUe~hnMY)K_y&6pm2^9nzLqfO+i$8EIyJj-^BS0Vps{z> zj$62M+`!n9Z(L@!?2^5X1mW)d{&DLJUyQedI)2d1SmlnsW$)QIRcjnGP}fto;S@B~ zQDwHYd#?geWKCj81tp4+%43M73i>&HN)^O!YSsQUwaXAW_RelVg*EO~RBR-;7`5+K zM!_MtKGW#k7!g82;ex}v>-uOCg2vV#{wCwBfrbchdG9ocgMZ5O0j^@E>`1DRY2=Ga zQ)qaSv`&+HW)`RGM#MvWIW=nQQ>v_4YCF!{BDH?=-LW(Fihxw;N#D7_#|i-2rQA!$ z*agQp-F=;&X}23U2rS5R#T|-ur~oa_?z-KBOpQGgXYzv+ha-c2g)l54H0$KW2Qf%i zEb?8wh-IFqExdP{JPY&|^Y>nM*(m)IIUS`0A7%9ftU#&J+O>_~V0~797av$GCW2!He zKgeTWg}ue#7eOcVCJR!bUD?JQ$GrAGE6}q~;`D!4pqia=(NiEoicK<=_lAyp8haYYxPp|&AG>DgFd9wVMZ<=x?IwX~sR5=smfRMNSax=0*o>y(qij(mBRc9^ z;573hbTSw1R;FVhI$4hOhs&=(H@P-W?7l>cnElemM;3qg--RL@bMVL6U&n#|ERT-( zSMZOE0`b$xA8a;7;SU{SdJG)uN9fifP8L2{0=hpF1NUdX6#38YkFA28UD3`3Rf(0I zm2;Dxu75;Yjv4r6gPwkfp>I&G<{_K?1QrMG-^4grGK=Hy%s4nWi|ZlZ2yV>+a3n!> zy>~EYQIz?Y?>_Meg<+s;^WlN+a{fFRFh7Nxz^K`PM*$4kfG1mE12lZFUiR}x$T}MU z3wRQXHo=OyJP#`W%FX^oW5anb z%bOFQ@xNvTg!o=pK7oNzhl7FP{fp-GT4H0M?+_a?SIVob5A7=UJ&O1)8(MNxF=+P zYZ{dcCD?|RziqB92hPmrepo%=xqJYuggkgGo)QcwpnYH@4M0u=Lp20IUnKWG{`lvC z_z5e44GUx*8OsX*uq>)jMq==G0p%lxUI_ICwFhCJm59LtMJSKB2NciNgYp8wUkiyJ zu_Y)rVDLaoT15Y#_^)%oKU$$R#1F~jLDBqM6maxELO&M)V8tyTgwmQ~-Ya_$5)c9^}kvC>#hDjHTO2@jD-&8<)c9KG5~CU{DUo2 z%P1Z(4=C1>^kBDlap?B~AMr6LF8m3K3xOHR|6u&(0NB8EutvG$gDFz0q(E>`Q`AD^ z3;SP6H9kL#1UdCzgGX_v@(4iJU;i^082-PA3$q@G_0j?NnqK@BkD-KQe-XdR1&?Hs Y<0;A@Kqn6dh6(zqg)WZv-24Cj4~6)YzW@LL delta 8312 zcmZWu1z1#1_g)s3?h=+RrDG}SZs`&UX{EaaTtQm;A`J>6EZveKASEFsAtj9<-J;@u ziSPIO^L_W(`^@aQ?|aUinRDl!nTgv1w=9A2wbW42ut6Y9OweurztRc#EO-}X6&$=x z1p$1iMO5FtG^>xhPG@T%dVIj6qGWdkg4^DWom#00J?7itG)A||#U&p5&ma&~V>8F>#^Y5Q|@b!}{WXmAd2J z&lW`qbA0vv;PpEG8E@W^1YYA2g;3^{a#W((PzmW@d_M7v_|v#Ao4(QVh>EF2cO_K~ zMq-UUL~s17p+!VdxxXE}m#`NqQKhn0K`FtbV#ge5fN?ORWapxy63DWg`0gr(MZRQ-b?X84)mlJs;;u zrlTVwRDC>=iW#@|u?v>)ubP#o=UgT%4nT?`k)ipbsFAhq2V~Sx{wKOFqGE0HzDE%< zs-}@QBM^j74*Wn60JUf}m%&r*1Q|rVs!3zjy>qkl-&gZrnKaC`w{y0(DNfq7^KCoRKXb#-{a595o&T;<6T+9(dv@Gvo zAh1T7*$v@)QWRVOUs$(1jD9kbT{v$$!?@po-{+4H_!_$C|Dj^4mZo+d&s_I12dy-C85wTjE>t_lo zCz!{>cJ8L?{H+d~E1Ab{R%kQc6N0#Oi;*^y>?H0vQ~I#OBNipF=U$vZcAr+Rp`rw6 zb%l5Aeze@nz2B?`GR2)wyt_V&5+Hv_SGuOX56dUp45yKnIi$mxryU+te!RUcGATkV zCgk=QQ7tyk&j*{~7jW}^U9Kd{>#ZnOte~6DM@jRpG>@;rusvP1C9*9is4Y9k%8EqE zzl)i36BbhfFQq(2B=@d&lrQeA(|H;FIA)2-n`OwkO-=byYue^^y@ea6-SzQuP%kg@@Y>dJ}O5-xRjJC%LKOMWPFBV$Y5WnxcS0Kp55HR6o zL&cZep$0#c$swy8H?;K9-^X0|_(?^s+Jk)Hs~AS$iN1%Pt|;aVf`#~HukPTkV?k8J zePfm4!(YUMEGyD@3+zIXO81wD@n&OVacL-9%qORQ=1%mGDD!o6)>D|#7Gv^E=rIZO zTkxw_v`Ri0crVghkygFfNafvEwAf-QUHH9In~VuwuiZ(>tZx#R7wxY<6W+&udzX~* zr1%s~ve1%Pmt8Vcv+p!I3S-v@izAS6H==&NFeyY|^2O|`2$PLH78b*+F`0qvjegrB zft`xCj7**zWU~HyB3N9KV)(o4HclEHeqm1PXvtB7OEOr~=W(f-rO7Ubg}#^%we}PO zR60}Odt_t}J10^nY(q29YG?YQA-5{uCt-J_wQw&{vA0CJ6rR4jyWz825w){uvDyM0 z#?;ER2Ul^yS_&3_od!*w$<2*6FinkmhaNk|BWk^vWG+Vp`+aT_k&KCUE?R~ z+Ol!@+_Q-}nHG0OpSWow_QWVZ817-@xNq5I!}U7p_U>TOn5J8#db8$?=G&!nlWN+1 zbqw3a%MvP$FK~{);`dCCyhHlPS?aCLkyc-qqng-`ippkdA^(#ZgS|(;6_kG$M}5F@ z!^>9i@KpIliu$ng+aBinONc_J)NVu+Ep<{w^ZsYL9u{4!h2t|=CVcT>v%;{qVQ>^2 z#}6L!w$x^oZFCfmN3lK6IpyKcRo7*Ew!lKVtj~kXn;LFG!3$9(9QztEY1J$OWTQRw zgBAw@jmfu{&$_v>^dcqinx$pVNN<()*xMLY0lyVxlMGiIMf9`FD(Jo?aEO?ff7>V_ zou|G(P9x7Qyq&Ths8G+=<1@fxb=O^9Ml1`S>1p5FwQFU_P+5?m)9=C56@xw~Fi&oz zEyLu&W_ge$%V|*nM=6$eeaF)48bR+bc)vZCJ+$~IQ)REQLWD#4_HNahW^o(?$HBTq zuS#UommVrDQ@M$s6Ma>;LOnbL)Px;{$$z}?Fnj6Rv9rPgynRTbU!LRsQe<+3pG`dp zF2$i1VY<*D4i4z7>38D5tF4#cB4!(`Mc*Q?Scanh#a zVG)xTk&h~kS6NERWVuR-%YWKCl<><(k#}HS*xD>f?Z>e-i}*% zyppX?d~G#)_$$4V2?jxTf#<>p(Lec_$?)9w>Fr|{P!SpQ+od&%QrpLFUG;;sQc6NU zH@)1WN$ii4958ttd3Jxb#eE~FkDc~h;>1V?ZnHS1jT)4LB89V^8ktQ#YF2Ii)+%ko z)_!;QL6F=g3-zim?Xm0AJjWJ-BkBXPZXre0r1u?9pZ*ZYYobn;eCLJlMsxPHh#M7- z&fC*MtJzN2(6Jg$NPmY{tF`&QF^XWIqx~5CT6|5kf*@_i&tYK2FZ+0n zQ^+M?DEWym&@90ktyA$Yxx92nS*#ngP%b<61cjZHlv*2wH zEE?t;?Lx5Y!)%WuN^SBIX>N&t#Sk4&L)=W$jG1B;q-6qn{e$26^Hn}ud$4n^y5`RG zrWn^U$m%hQTR!IS`W}~$XYbO3b73@Lbm5g@Sd1&nOb)DmMd9vKTVGcn>Dy96(-F$F z9rk_C-+gmG)Zolh%C&MD^_{U9sY8d;z3P59JTmo=X}&&kJ2^}%?Xg0=97K(T69TIVTD=l zC@eQ;V@NRvBkQ!G&^N12#1?EYU@rCXu+jXESI?4k#S3-XS)Vs@@}YXRzX*-{h_e3B z-Aur%ze}QlFOwVwRok^d2r^SgCh-iHNe&Mf6V7erXyM@zYA~%!rBCrUE8ZHg$;g+M z(|tnXfW<}V-zocot4@d%MI~p%;71~L`@25pnORb;A8)6gC=?seJfhB$yz8a-ON&mn zTlB2!IXvksi)C4sUpWl$XzAN&dIfy=`}2<`2xBiMdiL%et6O2HzA{~yiY(Eeb{7;V zmz3eXcjK@s??tTy69%Ysk#einGcQy{6PhJgVwLyQ2OHWTQj-&8r!Y=jtTp55{XqOM zSnA811$)rIqN{q(dT_2c zpKm>VX}!1zN_fzYdrp#`BO-w-SUIkO?(!;a6*nVX@Oj+wYJP*4S=M-@j<93CUZ75` zfEzdA`_CncX4}+4rT4_G3hlDG*SG}LK6m{Z@RJiB^A!FVn`GDjI4!etqNzL$U5?%q zPG~bpL)B7bX1Ijzvq-WOxjhEhc3}u+`QTu%2xh&jS2vJ*nM-P#J{@aIswMZb5qG~; zHtG>%G)=R4ro?pkky5M7~vwEp8+xXP$hRz})FlHK52v;Eedff$d4t<@46+YtUh zTMcuq)o{#?ei7V++@Hglz<`KK_u>XMJjjoYa(V6@Hj^Ib^MVi;=7r&i54DI_yuD*9 zd1=);CLQt$W@_%?ga)o4Cv5zj7|T?zOh+YyJpcC9WHhxPy#3-w&vmx`{B|QGk>$;Xzy$!-DHN%BYl>n@i_un$hBBMS)DBs;W-+@EDw;D@|giVfi*4Tvl!o#~{3Jbb|{;6%tV_N}(8 zSn;-`XyD5#&2s~7qlTjxSj?IuTEZ_c~#gsk^y#Lm7bk#R1yL|SrWitq4dJp$_xFs~3jlj~= znM-UZJmuIE7@E>Zaz4v1z-s%`XMVWj_dUvM2nv>9MB7r}2m3;fCc_utXm%2-lI;#M5+Vlej;!V|>Mo*0^3=?{&;ToA+Mu2J zK;>_3;>$R{2x_?AyhEc3@=ckhPk8W5`Xd?{!y4M}Z2bsu^CV3t--I=j=pHo#b;0`{ z)mvLl6VcWFA%%o%C{z}vEvSC1rsv(00!I`%Y*j6OlnV(6sW>jvIRsodu95p&a`G-+ z5I5T-W%OCD4(*X{H#nQljg@!?TL0D?ov967pdYex{_N}G1pmM{u1)Tt31D5`^H1quJtk>@;Y#7AW8|W?-WhOjG*j2ZP9t9J$)^DY0p$S|{lM zz3LBbQLn@fw=X*N>UZHXwGt{`-*XN#bNgTAYw{3#{>fI7;7ABzO>Wzo5%?sldSY&7I z9aL54W_zd3qk8X?%9#4+Ps{A=100|F)oX9;%1|Sh$N;J;2iirY8m0@2AzEzD(kC)lfu`~sHj4?RZq$EeH25`1!g#$I>%xlM4 zf42Sv{wi6H`vCug*V=T-9VM5l%SWd`wJPdLh(l8!6SuOdfosIswTqR!JW&;;X?4HN zlVVo1hNZ!yZH znu&8^O~<2o>IS(R%J|N(LO;x+}PP$pA}yw2E>A zTz($w(I&*}ZG&K5l6*UfJsbzk*r8+$2PKaVLDl@-Q2Dy|Ts<})`dGdnJsvNb6ybAH z);vT%%uE&+jGia*Zy2qidg2!oGwMpQgE-ZB9w~!|N}*R-mV*gbQ&9P6SYOtLUf4mk zQZ&DUO{=?&zfq#Sx=naWfA-*u(nq(zd73HjJ3X5QkI(J!;He!NYLKc;rt*d6b}&B(p) zIz9vKXT6)5`Mdmkgq40BZx3Cx+;1;zCWb#?h*G`%NA0JN)27ZSq&CaM==QrTT&(Ft zykXvGY-OV+?`6egsmt@sq^QThuJYPiVg^TK`Xafl#;ZV>=JFjvP^#qa%BAIykyX$npQ(TaZ2$kH_q$C>Gf%SBl+`=uQ!`lTl2;Gvl5AZo#Op{&-idXvd?>tz3Ct zs%|wc$j@i~DavqlHyLhMr7q9BV+Pf|LYmizKIf1r9mxuOf_rVgx`d^aJ$Av?>A?X# zU^WzytQhytLJ4mwMCtre%6T8$*3n(Z^F-f&IREwqx4esBiGe#AiDN{6KyBBuCc(XC znOmj95^9QjbweZWGaYj_v5|Gk`NEYuklgjt*4s~#MgEP>Ai;T3KpeE(WDAGLg4>xn^!;9nT&r0cW=z#vc;8ZeV62?y7jBxP_0 z50P7RHj!r#c7)^+0Z`m#ye(_Ws#z9)hDE8pS-RJeVf2w_n{lx0ucog_l>^e!JXXcN ziF^~`ho7IHVwq#rQlIe^TXKh^+4>KK>(JWh>GeNy;|a$l&wHO5%5d_FJ$dJrU zj?@}ImCRLBCa0wvKb7dT9S+-QKPbk20&TBJqpLS*yEk#&H5EtW)P z&pIC#)eX&_FAV;4ApRq{Q{7fDVD+GuK%I7kh;ebu82`j`dhf{8yVc80p_wrq2B8c&NU{~Uf`6Ej%4uNQ4jwy1G-!t{2(mhll0n~wS~B0#D5j4SsS2yEx!tn@ zDLb*xm#E}+WZd&`4ohF=`Jsl z>kcH2bQgLa2V2{|K|UKp-c0{sK%l&!)9WQS7Qlxfp})Xh9(^Gdx+0S! z`5V|76d(bO4B!Ft0YwPhtJXInpkO^Tq?{Qz`;QE}5DJ!rTw>!PP>%n03eJ#UvWirG zvEt{wVSosDRw#AZ2o4P=EGROeb3>k(+`fTPz`s=dsu&M;wMp>b>Tevh+@QW8LsH2B zl_HAEPEneHcC;hg@e&IJ;<^E>2c8v)U-{Qs1O<1pUHgIo(6ZxS_iq2ywoKSxd8&;l@MQo|4Fuyz?&n7j6kUpzRQRO8cU#3 zi3(RSbR8yEj&?Uf{-4WmfI8nX;psC|KFDv1lU8HxO^n=JMw0| zK{!zcEbN$om!%|EEhs64f|pb-Wj{*q&|i>gpJS(7d=8!?cXuRikmv3pfoy zs0<43*F?&wfrc`sE7^P*6vYDIE4xVLS+7KD3nA{mqs#Lxt^Whd2YazWxLwc=v!|M%~%VtJ>47}Hb!4&;fE)zz8wP4$UV88>oeKC}bFB=w779L1172r~{g0x| zY85ytvOnvPZz}ADqP~nvMai#;{u#MAp-f?Mq}5*JD=l^dOq+cPUeASISiKyne`azd hknRS!ItMV$XCT*7gCL{eqJua=ZUi8ZaPGyw{{tLfb9Dd! diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9829a99a5b..da2f8ac95a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Aug 14 16:28:54 PDT 2012 +#Tue Mar 05 10:18:48 PST 2013 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=http\://services.gradle.org/distributions/gradle-1.1-bin.zip +distributionUrl=http\://services.gradle.org/distributions/gradle-1.4-bin.zip diff --git a/gradlew b/gradlew index e61422d06d..91a7e269e1 100755 --- a/gradlew +++ b/gradlew @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash ############################################################################## ## @@ -61,9 +61,9 @@ while [ -h "$PRG" ] ; do fi done SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" +cd "`dirname \"$PRG\"`/" >&- APP_HOME="`pwd -P`" -cd "$SAVED" +cd "$SAVED" >&- CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar From 84339e0a40358919863496d6fd8c6035228b6329 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Tue, 5 Mar 2013 11:30:53 -0700 Subject: [PATCH 031/357] Switching to bintray for dependencies (same as Maven Central) --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 7f8fb72deb..80e2f17fb2 100644 --- a/build.gradle +++ b/build.gradle @@ -2,12 +2,12 @@ ext.githubProjectName = rootProject.name // Change if github project name is not the same as the root project's name buildscript { - repositories { mavenCentral() } + repositories { mavenRepo url: 'http://jcenter.bintray.com' } apply from: file('gradle/buildscript.gradle'), to: buildscript } allprojects { - repositories { mavenCentral() } + repositories { mavenRepo url: 'http://jcenter.bintray.com' } } apply from: file('gradle/convention.gradle') From d87b66e0d0074d42b35e46a5c3664dd57294e16d Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Fri, 22 Mar 2013 13:55:49 -0700 Subject: [PATCH 032/357] Move gradle-release dependency to bintray --- gradle/buildscript.gradle | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/gradle/buildscript.gradle b/gradle/buildscript.gradle index 4d6a29aabe..d3f06ec892 100644 --- a/gradle/buildscript.gradle +++ b/gradle/buildscript.gradle @@ -1,13 +1,10 @@ // Executed in context of buildscript repositories { // Repo in addition to maven central - maven { - name 'build-repo' - url 'https://raw.github.com/Netflix-Skunkworks/build-repo/master/releases/' // gradle-release/gradle-release/1.0-SNAPSHOT/gradle-release-1.0-SNAPSHOT.jar - } + repositories { maven { url 'http://dl.bintray.com/content/netflixoss/external-gradle-plugins/' } } // For gradle-release } dependencies { classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.6.0' classpath 'com.mapvine:gradle-cobertura-plugin:0.1' - classpath 'gradle-release:gradle-release:1.0-SNAPSHOT' + classpath 'gradle-release:gradle-release:1.1' } From c3477877da453c81a3c3c121d7677fce2911551d Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Fri, 22 Mar 2013 17:13:07 -0700 Subject: [PATCH 033/357] Use newer version of license-gradle-plugin that fixes skipExistingHeaders field --- build.gradle | 6 ++++-- gradle/buildscript.gradle | 2 +- gradle/license.gradle | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 80e2f17fb2..582aa14d17 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,10 @@ ext.githubProjectName = rootProject.name // Change if github project name is not the same as the root project's name buildscript { - repositories { mavenRepo url: 'http://jcenter.bintray.com' } + repositories { + mavenLocal() + maven { url 'http://jcenter.bintray.com' } + } apply from: file('gradle/buildscript.gradle'), to: buildscript } @@ -44,4 +47,3 @@ project(':template-server') { compile project(':template-client') } } - diff --git a/gradle/buildscript.gradle b/gradle/buildscript.gradle index d3f06ec892..2cb8e60a16 100644 --- a/gradle/buildscript.gradle +++ b/gradle/buildscript.gradle @@ -4,7 +4,7 @@ repositories { repositories { maven { url 'http://dl.bintray.com/content/netflixoss/external-gradle-plugins/' } } // For gradle-release } dependencies { - classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.6.0' + classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.6.1' classpath 'com.mapvine:gradle-cobertura-plugin:0.1' classpath 'gradle-release:gradle-release:1.1' } diff --git a/gradle/license.gradle b/gradle/license.gradle index 11a51f1137..abd2e2c0e1 100644 --- a/gradle/license.gradle +++ b/gradle/license.gradle @@ -5,5 +5,6 @@ apply plugin: 'license' //nl.javadude.gradle.plugins.license.LicensePlugin license { header rootProject.file('codequality/HEADER') ext.year = Calendar.getInstance().get(Calendar.YEAR) + skipExistingHeaders true } } From 883dd0daf391695a3b26e1d0f5c1bb112022c803 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Tue, 26 Mar 2013 12:20:52 -0700 Subject: [PATCH 034/357] Add sonatype snapshot repository --- gradle/maven.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gradle/maven.gradle b/gradle/maven.gradle index a3a3d44240..3bf788d3e8 100644 --- a/gradle/maven.gradle +++ b/gradle/maven.gradle @@ -24,6 +24,10 @@ subprojects { authentication(userName: rootProject.sonatypeUsername, password: rootProject.sonatypePassword) } + snapshotRepository(url: 'https://oss.sonatype.org/content/repositories/snapshots/') { + authentication(userName: rootProject.sonatypeUsername, password: rootProject.sonatypePassword) + } + // Prevent datastamp from being appending to artifacts during deployment uniqueVersion = false From 63e44e8e73b9fdfe56655cc1fb13180885dedc9e Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Tue, 26 Mar 2013 12:52:41 -0700 Subject: [PATCH 035/357] Using latest features of release plugin --- gradle/release.gradle | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/gradle/release.gradle b/gradle/release.gradle index cd135643bd..669c1db684 100644 --- a/gradle/release.gradle +++ b/gradle/release.gradle @@ -7,12 +7,11 @@ task release(overwrite: true, dependsOn: commitNewVersion) << { commitNewVersion.dependsOn updateVersion updateVersion.dependsOn createReleaseTag createReleaseTag.dependsOn preTagCommit -def buildTasks = tasks.matching { it.name =~ /:build/ } -preTagCommit.dependsOn buildTasks +preTagCommit.dependsOn build +preTagCommit.dependsOn buildWithArtifactory preTagCommit.dependsOn checkSnapshotDependencies -//checkSnapshotDependencies.dependsOn confirmReleaseVersion // Introduced in 1.0, forces readLine -//confirmReleaseVersion.dependsOn unSnapshotVersion -checkSnapshotDependencies.dependsOn unSnapshotVersion // Remove once above is fixed +checkSnapshotDependencies.dependsOn confirmReleaseVersion +confirmReleaseVersion.dependsOn unSnapshotVersion unSnapshotVersion.dependsOn checkUpdateNeeded checkUpdateNeeded.dependsOn checkCommitNeeded checkCommitNeeded.dependsOn initScmPlugin @@ -30,23 +29,18 @@ checkCommitNeeded.dependsOn initScmPlugin tasks = [ 'build', value ] } } -task releaseArtifactory(dependsOn: [checkSnapshotDependencies, uploadArtifactory]) +task releaseArtifactory(dependsOn: [preTagCommit, uploadArtifactory]) // Ensure upload happens before taggging but after all pre-checks -releaseArtifactory.dependsOn checkSnapshotDependencies createReleaseTag.dependsOn releaseArtifactory -gradle.taskGraph.whenReady { taskGraph -> - if ( taskGraph.hasTask(uploadArtifactory) && rootProject.status == 'release' && !taskGraph.hasTask(':release') ) { - throw new GradleException('"release" task has to be run before uploading a release to Artifactory') - } -} + subprojects.each { project -> - project.uploadMavenCentral.dependsOn rootProject.checkSnapshotDependencies + project.uploadMavenCentral.dependsOn rootProject.preTagCommit rootProject.createReleaseTag.dependsOn project.uploadMavenCentral gradle.taskGraph.whenReady { taskGraph -> - if ( taskGraph.hasTask(project.uploadMavenCentral) && !taskGraph.hasTask(':release') ) { - throw new GradleException('"release" task has to be run before uploading to Maven Central') + if ( rootProject.status == 'release' && !taskGraph.hasTask(':release') ) { + throw new GradleException('"release" task has to be run before uploading a release') } } } @@ -55,11 +49,12 @@ subprojects.each { project -> ext.'gradle.release.useAutomaticVersion' = "true" release { - // http://tellurianring.com/wiki/gradle/release failOnCommitNeeded=true failOnPublishNeeded=true + failOnSnapshotDependencies=true failOnUnversionedFiles=true failOnUpdateNeeded=true includeProjectNameInTag=true + revertOnFail=true requireBranch = null } From 8caf8ec93b3617749db90ceac64e06116567f072 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Tue, 26 Mar 2013 12:52:41 -0700 Subject: [PATCH 036/357] Upgrading release process --- build.gradle | 6 ++-- gradle/buildscript.gradle | 2 +- gradle/check.gradle | 41 +++++++++++----------- gradle/convention.gradle | 13 ++----- gradle/maven.gradle | 19 ++++++----- gradle/release.gradle | 71 ++++++++++++++++++--------------------- 6 files changed, 72 insertions(+), 80 deletions(-) diff --git a/build.gradle b/build.gradle index 582aa14d17..5e6c63bb99 100644 --- a/build.gradle +++ b/build.gradle @@ -4,13 +4,15 @@ ext.githubProjectName = rootProject.name // Change if github project name is not buildscript { repositories { mavenLocal() - maven { url 'http://jcenter.bintray.com' } + mavenCentral() // maven { url 'http://jcenter.bintray.com' } } apply from: file('gradle/buildscript.gradle'), to: buildscript } allprojects { - repositories { mavenRepo url: 'http://jcenter.bintray.com' } + repositories { + mavenCentral() // maven { url: 'http://jcenter.bintray.com' } + } } apply from: file('gradle/convention.gradle') diff --git a/gradle/buildscript.gradle b/gradle/buildscript.gradle index 2cb8e60a16..b6fb61e167 100644 --- a/gradle/buildscript.gradle +++ b/gradle/buildscript.gradle @@ -6,5 +6,5 @@ repositories { dependencies { classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.6.1' classpath 'com.mapvine:gradle-cobertura-plugin:0.1' - classpath 'gradle-release:gradle-release:1.1' + classpath 'gradle-release:gradle-release:1.1.4' } diff --git a/gradle/check.gradle b/gradle/check.gradle index 7617f17b35..a3e4b4e7f5 100644 --- a/gradle/check.gradle +++ b/gradle/check.gradle @@ -1,25 +1,26 @@ subprojects { - // Checkstyle - apply plugin: 'checkstyle' - tasks.withType(Checkstyle) { ignoreFailures = true } - checkstyle { - ignoreFailures = true // Waiting on GRADLE-2163 - configFile = rootProject.file('codequality/checkstyle.xml') - } +// Checkstyle +apply plugin: 'checkstyle' +checkstyle { + ignoreFailures = true + configFile = rootProject.file('codequality/checkstyle.xml') +} - // FindBugs - apply plugin: 'findbugs' - //tasks.withType(Findbugs) { reports.html.enabled true } +// FindBugs +apply plugin: 'findbugs' +findbugs { + ignoreFailures = true +} - // PMD - apply plugin: 'pmd' - //tasks.withType(Pmd) { reports.html.enabled true } +// PMD +apply plugin: 'pmd' +//tasks.withType(Pmd) { reports.html.enabled true } - apply plugin: 'cobertura' - cobertura { - sourceDirs = sourceSets.main.java.srcDirs - format = 'html' - includes = ['**/*.java', '**/*.groovy'] - excludes = [] - } +apply plugin: 'cobertura' +cobertura { + sourceDirs = sourceSets.main.java.srcDirs + format = 'html' + includes = ['**/*.java', '**/*.groovy'] + excludes = [] +} } diff --git a/gradle/convention.gradle b/gradle/convention.gradle index 4f07d1a64b..36650a2f98 100644 --- a/gradle/convention.gradle +++ b/gradle/convention.gradle @@ -1,15 +1,11 @@ - -// For Artifactory -rootProject.status = version.contains('-SNAPSHOT')?'snapshot':'release' +status = version.contains('SNAPSHOT')?'snapshot':status subprojects { project -> apply plugin: 'java' // Plugin as major conventions - version = rootProject.version - sourceCompatibility = 1.6 - // GRADLE-2087 workaround, perform after java plugin + // Restore status after Java plugin status = rootProject.status task sourcesJar(type: Jar, dependsOn:classes) { @@ -51,9 +47,6 @@ subprojects { project -> } } - // Ensure output is on a new line - javadoc.doFirst { println "" } - configurations { provided { description = 'much like compile, but indicates you expect the JDK or a container to provide it. It is only available on the compilation classpath, and is not transitive.' @@ -79,5 +72,5 @@ task aggregateJavadoc(type: Javadoc) { // Generate wrapper, which is distributed as part of source to alleviate the need of installing gradle task createWrapper(type: Wrapper) { - gradleVersion = '1.4' + gradleVersion = '1.5' } diff --git a/gradle/maven.gradle b/gradle/maven.gradle index 3bf788d3e8..b924757625 100644 --- a/gradle/maven.gradle +++ b/gradle/maven.gradle @@ -8,14 +8,16 @@ subprojects { sign configurations.archives } - /** - * Publishing to Maven Central example provided from http://jedicoder.blogspot.com/2011/11/automated-gradle-project-deployment-to.html - */ - task uploadMavenCentral(type:Upload, dependsOn: signArchives) { - configuration = configurations.archives - doFirst { - repositories.mavenDeployer { - beforeDeployment { org.gradle.api.artifacts.maven.MavenDeployment deployment -> signing.signPom(deployment) } +/** + * Publishing to Maven Central example provided from http://jedicoder.blogspot.com/2011/11/automated-gradle-project-deployment-to.html + * artifactory will execute uploadArchives to force generation of ivy.xml, and we don't want that to trigger an upload to maven + * central, so using custom upload task. + */ +task uploadMavenCentral(type:Upload, dependsOn: signArchives) { + configuration = configurations.archives + onlyIf { ['release', 'snapshot'].contains(project.status) } + repositories.mavenDeployer { + beforeDeployment { signing.signPom(it) } // To test deployment locally, use the following instead of oss.sonatype.org //repository(url: "file://localhost/${rootProject.rootDir}/repo") @@ -62,5 +64,4 @@ subprojects { } } } - } } diff --git a/gradle/release.gradle b/gradle/release.gradle index 669c1db684..23a1a68227 100644 --- a/gradle/release.gradle +++ b/gradle/release.gradle @@ -1,47 +1,49 @@ apply plugin: 'release' -// Ignore release plugin's task because it calls out via GradleBuild. This is a good place to put an email to send out -task release(overwrite: true, dependsOn: commitNewVersion) << { - // This is a good place to put an email to send out -} -commitNewVersion.dependsOn updateVersion -updateVersion.dependsOn createReleaseTag -createReleaseTag.dependsOn preTagCommit -preTagCommit.dependsOn build -preTagCommit.dependsOn buildWithArtifactory -preTagCommit.dependsOn checkSnapshotDependencies -checkSnapshotDependencies.dependsOn confirmReleaseVersion -confirmReleaseVersion.dependsOn unSnapshotVersion -unSnapshotVersion.dependsOn checkUpdateNeeded -checkUpdateNeeded.dependsOn checkCommitNeeded -checkCommitNeeded.dependsOn initScmPlugin - -[ - uploadIvyLocal: 'uploadLocal', - uploadArtifactory: 'artifactoryPublish', // Call out to compile against internal repository - buildWithArtifactory: 'build' // Build against internal repository -].each { key, value -> +[ uploadIvyLocal: 'uploadLocal', uploadArtifactory: 'artifactoryPublish', buildWithArtifactory: 'build' ].each { key, value -> // Call out to compile against internal repository task "${key}"(type: GradleBuild) { startParameter = project.gradle.startParameter.newInstance() + doFirst { + startParameter.projectProperties = [status: project.status] + } startParameter.addInitScript( file('gradle/netflix-oss.gradle') ) startParameter.getExcludedTaskNames().add('check') tasks = [ 'build', value ] } } -task releaseArtifactory(dependsOn: [preTagCommit, uploadArtifactory]) + +// Marker task for following code to key in on +task releaseCandidate(dependsOn: release) +task forceCandidate { + onlyIf { gradle.taskGraph.hasTask(releaseCandidate) } + doFirst { project.status = 'candidate' } +} +release.dependsOn(forceCandidate) + +task releaseSnapshot(dependsOn: [uploadArtifactory, uploadMavenCentral]) + +// Ensure our versions look like the project status before publishing +task verifyStatus << { + def hasSnapshot = version.contains('-SNAPSHOT') + if (project.status == 'snapshot' && !hasSnapshot) { + throw new GradleException("Version (${version}) needs -SNAPSHOT if publishing snapshot") + } +} +uploadArtifactory.dependsOn(verifyStatus) +uploadMavenCentral.dependsOn(verifyStatus) // Ensure upload happens before taggging but after all pre-checks -createReleaseTag.dependsOn releaseArtifactory +createReleaseTag.dependsOn([uploadArtifactory, uploadMavenCentral]) -subprojects.each { project -> - project.uploadMavenCentral.dependsOn rootProject.preTagCommit - rootProject.createReleaseTag.dependsOn project.uploadMavenCentral +gradle.taskGraph.whenReady { taskGraph -> + def hasRelease = taskGraph.hasTask('commitNewVersion') + def indexOf = { return taskGraph.allTasks.indexOf(it) } - gradle.taskGraph.whenReady { taskGraph -> - if ( rootProject.status == 'release' && !taskGraph.hasTask(':release') ) { - throw new GradleException('"release" task has to be run before uploading a release') - } + if (hasRelease) { + assert indexOf(build) < indexOf(unSnapshotVersion), 'build target has to be after unSnapshotVersion' + assert indexOf(uploadMavenCentral) < indexOf(preTagCommit), 'preTagCommit has to be after uploadMavenCentral' + assert indexOf(uploadArtifactory) < indexOf(preTagCommit), 'preTagCommit has to be after uploadArtifactory' } } @@ -49,12 +51,5 @@ subprojects.each { project -> ext.'gradle.release.useAutomaticVersion' = "true" release { - failOnCommitNeeded=true - failOnPublishNeeded=true - failOnSnapshotDependencies=true - failOnUnversionedFiles=true - failOnUpdateNeeded=true - includeProjectNameInTag=true - revertOnFail=true - requireBranch = null + git.requireBranch = null } From 498c25feaa03a9a3a09e633814c192ddc770ced4 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Tue, 2 Apr 2013 11:48:33 -0700 Subject: [PATCH 037/357] Matching wrapper to 1.5 --- gradle/wrapper/gradle-wrapper.jar | Bin 46735 -> 46742 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 42d9b0e9c5872910311a1d035995ab8ec466e7ac..faa569a9a0eedc9ff37450fed24a7efd77a86729 100644 GIT binary patch delta 1766 zcmZWp3ou+)7(RPEYca_URV;%FkcmMtF!lyq^dKqZ1P8>#luLktBR1bVmC9$S*?ScWxu&(q6qrvyrAK z-KKOHmCs=SU=3hNn@z5d?92DQwl|-tG~qk-ui1V z_S|_fJ#?fp=h0|Q!ht9u+p4cT$7z1$qdcqJxrTDy4m@Hkw#0l93fel&2GHo1=EMs71D(WE{8`{%OI7l~} zaF8}E6@CU5)ZRV6JL#`R`=(|BZ~!LC9HCbGitc)fmX?sE`p^=E=~FZlli5Y5G0U1Y z$LdR$)@f$`@&aw#E@m6h6_hdQeD}(q(o17id0WcWfh_D(T02QIiaI&%(_df4G@^=OkW)j}q^18_?1{qinAuI3Oma8RNVVa(C^V(iuUWBf}a!+5mm z9L9krDMsVwXpD;HSd7EX5g7R`VKm=DuYc8Y0)JbzMqzxn^$f=8*6{z#x1cS=j%q-E zGSP@`J&pM+sBCkg`iBE;WWZ)90O!(^Mak)jinOz$GifOzc^HGRWYQO&k}CTB&xAz+ zfD$$Up8x6~Y|M9)z(9$Y+s*)S{8^gpVOxXvQ+g)(U zdkQ#}5alT_0N@S1un_3k5st(1I!MAd5{22qfsTEcnn%=F6jrk96ktlyNfIrm5#be@=Ql6ZUxYmh~|VA~e`1)}d?Mzkwr=&b2RbtExV zjE?G6xK`}=zvF_R4O$JN675lm?s_GT!5cb1EHRHHrpmD@uYyl)45lSLByp<>(YA2B zmdD#M2=E444ync^=2eq$vxkl{=_QFPwXm+%&wWQkP>n7$mx*fWX8|DAt0k@u>r~WP yygU}AZ_WP0=l)v(?C=Ue+q5=U5GEfi@l|V}g66-pVReDBPppl1_?S3VTL zjf@crwWdTqVTFwFXD<@+lmLJy=;6X5^VLvca0^qiS^Z3Ggc|c$*vKwDr*aD zjJul9M>Cs?O|a+imXB!TSqO{3p2@Ip*v6DpdgW@<82D9W_@3FmpW>$Yy>UJ*u|7T; zUj|pe`rubfmJ4%dM$=LGyz1vsY5J2_^6qmL@x!AP(M|G!8`9pP_++oQS}Z{sulc3h zb)drk1<)bN@ypX4x!q2d#RaJwwsl|1+1Fol(l!Nd%4sdz?s&Gy=44~=7gt>ahI=P2 zP8SSy6+}mJ2KqKTw|#rhB0sB^PW(M2A?bxT&frroRm*fGz1dr2f z)`wm%lIq@%oS6T-@^zAP_FX|rV$Bt^fa@2=z0_7M-hsa}`5w2Wm7_Lxm%=sS&w>{1 zXBLP1F7GWIeOxvA8CS)YNJ?j_f+auBR?Vljvn0Jj?VlEh4ZA|J*DpFw^jNH19_kDl z^F5gewSjy;CS|}};dAUrig`_7LoE7>Hg?8K8Q3!@9%5tTO3DTtqWlOZ|$?UD$RK{@$UTyba4Wdi>H^zJ06oen_Mt4JITMjn2r8z)&OkRBX)~*f+i9N_&`kyikeB{ za>~*X2TrC~>O@D5R2_PX1ancoP`zCD({#CJo87T+)cncYa|QrFQvlrb$m|1{yidIb z6X$;E4^g026lezn^@zJOF;T5{$5S6xlZ+kkjoKf-@iZjk5EN>n@S99S62g345sx%H z?B=wP1mhrfhaEJ7vR1ms{m4CZT*sc0v*5-Poc~>x=uY~nQq*q+3jja8$_7f7f^DF= z*4mM}*@bJnT{Ng)XEOkT^vKs?sFsh*Ii)3uZYi8obK!FL0X-$nGNhqI3!#5M_s%bA zaG+-^7srcQN#bT1a(hraho=|TSHpXbCRd2YtiMn@%IWxWuE)Du2tkmn5RH5u32)V_ zwHSJ}MPjW(Z6vX`dZ`39tgN=ut;i#Eq?GH9rsVc|3d>)H2$XpjdK~o>LSbsVuVI&h Ga{dRU8)+K= diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index da2f8ac95a..061b536b4b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Mar 05 10:18:48 PST 2013 +#Tue Apr 02 11:45:56 PDT 2013 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=http\://services.gradle.org/distributions/gradle-1.4-bin.zip +distributionUrl=http\://services.gradle.org/distributions/gradle-1.5-bin.zip From 832eb537e9df36a2e53a90e1f34e2008a36f3f17 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Tue, 2 Apr 2013 17:30:25 -0700 Subject: [PATCH 038/357] Automatically aggregate and publish docs (java,groovy,scala) --- gradle/buildscript.gradle | 1 + gradle/convention.gradle | 33 ++++++++++++++++++++++++++++----- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/gradle/buildscript.gradle b/gradle/buildscript.gradle index b6fb61e167..0f6555176f 100644 --- a/gradle/buildscript.gradle +++ b/gradle/buildscript.gradle @@ -7,4 +7,5 @@ dependencies { classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.6.1' classpath 'com.mapvine:gradle-cobertura-plugin:0.1' classpath 'gradle-release:gradle-release:1.1.4' + classpath 'org.ajoberstar:gradle-git:0.5.0' } diff --git a/gradle/convention.gradle b/gradle/convention.gradle index 36650a2f98..1056bd5de9 100644 --- a/gradle/convention.gradle +++ b/gradle/convention.gradle @@ -63,11 +63,34 @@ subprojects { project -> } } -task aggregateJavadoc(type: Javadoc) { - description = 'Aggregate all subproject docs into a single docs directory' - source subprojects.collect {project -> project.sourceSets.main.allJava } - classpath = files(subprojects.collect {project -> project.sourceSets.main.compileClasspath}) - destinationDir = new File(projectDir, 'doc') +apply plugin: 'github-pages' // Used to create publishGhPages task + +def docTasks = [:] +[Javadoc,ScalaDoc,Groovydoc].each{ Class docClass -> + def allSources = allprojects.tasks*.withType(docClass).flatten()*.source + if (allSources) { + def shortName = docClass.simpleName.toLowerCase() + def docTask = task "aggregate${shortName.capitalize()}"(type: docClass, description: "Aggregate subproject ${shortName}s") { + source = allSources + doFirst { + def classpaths = allprojects.findAll { it.plugins.hasPlugin(JavaPlugin) }.collect { it.sourceSets.main.compileClasspath } + classpath = files(classpaths) + } + } + docTasks[shortName] = docTask + processGhPages.dependsOn(docTask) + } +} + +githubPages { + repoUri = "git@github.com:quidryan/${rootProject.githubProjectName}.git" + pages { + docTasks.each { shortName, docTask -> + from(docTask.outputs.files) { + into "docs/${shortName}" + } + } + } } // Generate wrapper, which is distributed as part of source to alleviate the need of installing gradle From 9de561434f303b8020269984d7ef313428414715 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Tue, 2 Apr 2013 17:39:50 -0700 Subject: [PATCH 039/357] Make uploadMavenCentral task, that encompasses other tasks --- gradle/release.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/gradle/release.gradle b/gradle/release.gradle index 23a1a68227..9daa84736f 100644 --- a/gradle/release.gradle +++ b/gradle/release.gradle @@ -21,6 +21,7 @@ task forceCandidate { } release.dependsOn(forceCandidate) +task uploadMavenCentral(dependsOn: subprojects.tasks.uploadMavenCentral) task releaseSnapshot(dependsOn: [uploadArtifactory, uploadMavenCentral]) // Ensure our versions look like the project status before publishing From d0e42e3dfb45c8d792c6cced1c943c89b1d861dc Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Mon, 8 Apr 2013 10:04:45 -0700 Subject: [PATCH 040/357] Handle unavailable sonatype properties --- gradle/maven.gradle | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/gradle/maven.gradle b/gradle/maven.gradle index b924757625..817846d77f 100644 --- a/gradle/maven.gradle +++ b/gradle/maven.gradle @@ -22,12 +22,15 @@ task uploadMavenCentral(type:Upload, dependsOn: signArchives) { // To test deployment locally, use the following instead of oss.sonatype.org //repository(url: "file://localhost/${rootProject.rootDir}/repo") + def sonatypeUsername = rootProject.hasProperty('sonatypeUsername')?rootProject.sonatypeUsername:'' + def sonatypePassword = rootProject.hasProperty('sonatypePassword')?rootProject.sonatypePassword:'' + repository(url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2') { - authentication(userName: rootProject.sonatypeUsername, password: rootProject.sonatypePassword) + authentication(userName: sonatypeUsername, password: sonatypePassword) } snapshotRepository(url: 'https://oss.sonatype.org/content/repositories/snapshots/') { - authentication(userName: rootProject.sonatypeUsername, password: rootProject.sonatypePassword) + authentication(userName: sonatypeUsername, password: sonatypePassword) } // Prevent datastamp from being appending to artifacts during deployment From 28c6989099fc538cfd9d1d8d13075901bcec769d Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Fri, 12 Apr 2013 12:29:08 -0700 Subject: [PATCH 041/357] Verify before we can't take it back, use preferredVersion variable --- gradle/buildscript.gradle | 2 +- gradle/convention.gradle | 3 ++- gradle/release.gradle | 11 ++++++++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/gradle/buildscript.gradle b/gradle/buildscript.gradle index 0f6555176f..0b6da7ce84 100644 --- a/gradle/buildscript.gradle +++ b/gradle/buildscript.gradle @@ -6,6 +6,6 @@ repositories { dependencies { classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.6.1' classpath 'com.mapvine:gradle-cobertura-plugin:0.1' - classpath 'gradle-release:gradle-release:1.1.4' + classpath 'gradle-release:gradle-release:1.1.5' classpath 'org.ajoberstar:gradle-git:0.5.0' } diff --git a/gradle/convention.gradle b/gradle/convention.gradle index 1056bd5de9..2720c8b402 100644 --- a/gradle/convention.gradle +++ b/gradle/convention.gradle @@ -1,4 +1,5 @@ -status = version.contains('SNAPSHOT')?'snapshot':status +// GRADLE-2087 workaround, perform after java plugin +status = project.hasProperty('preferredStatus')?project.preferredStatus:(version.contains('SNAPSHOT')?'snapshot':'release') subprojects { project -> apply plugin: 'java' // Plugin as major conventions diff --git a/gradle/release.gradle b/gradle/release.gradle index 9daa84736f..06acd17da8 100644 --- a/gradle/release.gradle +++ b/gradle/release.gradle @@ -19,7 +19,11 @@ task forceCandidate { onlyIf { gradle.taskGraph.hasTask(releaseCandidate) } doFirst { project.status = 'candidate' } } -release.dependsOn(forceCandidate) +task forceRelease { + onlyIf { !gradle.taskGraph.hasTask(releaseCandidate) } + doFirst { project.status = 'release' } +} +release.dependsOn([forceCandidate, forceRelease]) task uploadMavenCentral(dependsOn: subprojects.tasks.uploadMavenCentral) task releaseSnapshot(dependsOn: [uploadArtifactory, uploadMavenCentral]) @@ -34,8 +38,9 @@ task verifyStatus << { uploadArtifactory.dependsOn(verifyStatus) uploadMavenCentral.dependsOn(verifyStatus) -// Ensure upload happens before taggging but after all pre-checks -createReleaseTag.dependsOn([uploadArtifactory, uploadMavenCentral]) +// Ensure upload happens before taggging, hence upload failures will leave repo in a revertable state +preTagCommit.dependsOn([uploadArtifactory, uploadMavenCentral]) + gradle.taskGraph.whenReady { taskGraph -> def hasRelease = taskGraph.hasTask('commitNewVersion') From b0585dae4cfe7108fd8bd80d7c4b890f95e42119 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Fri, 12 Apr 2013 13:27:39 -0700 Subject: [PATCH 042/357] Passing status to Artifactory builds --- gradle/release.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/release.gradle b/gradle/release.gradle index 06acd17da8..7979dc3a18 100644 --- a/gradle/release.gradle +++ b/gradle/release.gradle @@ -5,7 +5,7 @@ apply plugin: 'release' task "${key}"(type: GradleBuild) { startParameter = project.gradle.startParameter.newInstance() doFirst { - startParameter.projectProperties = [status: project.status] + startParameter.projectProperties = [status: project.status, preferredStatus: project.status] } startParameter.addInitScript( file('gradle/netflix-oss.gradle') ) startParameter.getExcludedTaskNames().add('check') From 6df65bf346344a1b29abc51c2f3e866b6a9d2c55 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Thu, 20 Jun 2013 13:19:06 -0700 Subject: [PATCH 043/357] Fixing aggregateJavadoc --- gradle/convention.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gradle/convention.gradle b/gradle/convention.gradle index 2720c8b402..c4658fc33e 100644 --- a/gradle/convention.gradle +++ b/gradle/convention.gradle @@ -73,6 +73,7 @@ def docTasks = [:] def shortName = docClass.simpleName.toLowerCase() def docTask = task "aggregate${shortName.capitalize()}"(type: docClass, description: "Aggregate subproject ${shortName}s") { source = allSources + destinationDir = file("${project.buildDir}/docs/${shortName}") doFirst { def classpaths = allprojects.findAll { it.plugins.hasPlugin(JavaPlugin) }.collect { it.sourceSets.main.compileClasspath } classpath = files(classpaths) @@ -84,7 +85,7 @@ def docTasks = [:] } githubPages { - repoUri = "git@github.com:quidryan/${rootProject.githubProjectName}.git" + repoUri = "git@github.com:Netflix/${rootProject.githubProjectName}.git" pages { docTasks.each { shortName, docTask -> from(docTask.outputs.files) { From 222fc122e2438b30b553f9c0c8b6db80f417ec2a Mon Sep 17 00:00:00 2001 From: adriancole Date: Wed, 26 Jun 2013 18:07:32 -0700 Subject: [PATCH 044/357] initial import --- .gitignore | 8 +- README.md | 102 +++- build.gradle | 30 +- feign-core/src/main/java/feign/Client.java | 114 ++++ feign-core/src/main/java/feign/Contract.java | 120 ++++ feign-core/src/main/java/feign/Feign.java | 148 +++++ .../src/main/java/feign/FeignException.java | 51 ++ .../src/main/java/feign/MethodHandler.java | 127 +++++ .../src/main/java/feign/MethodMetadata.java | 76 +++ .../src/main/java/feign/ReflectiveFeign.java | 247 ++++++++ feign-core/src/main/java/feign/Request.java | 113 ++++ .../src/main/java/feign/RequestTemplate.java | 533 ++++++++++++++++++ feign-core/src/main/java/feign/Response.java | 183 ++++++ .../main/java/feign/RetryableException.java | 48 ++ feign-core/src/main/java/feign/Retryer.java | 66 +++ feign-core/src/main/java/feign/Target.java | 98 ++++ feign-core/src/main/java/feign/Wire.java | 139 +++++ .../main/java/feign/codec/BodyEncoder.java | 41 ++ .../src/main/java/feign/codec/Decoder.java | 84 +++ .../src/main/java/feign/codec/Decoders.java | 121 ++++ .../main/java/feign/codec/ErrorDecoder.java | 130 +++++ .../main/java/feign/codec/FormEncoder.java | 27 + .../src/main/java/feign/codec/SAXDecoder.java | 50 ++ .../java/feign/codec/ToStringDecoder.java | 12 + .../src/test/java/feign/ContractTest.java | 126 +++++ .../test/java/feign/DefaultRetryerTest.java | 59 ++ feign-core/src/test/java/feign/FeignTest.java | 168 ++++++ .../test/java/feign/RequestTemplateTest.java | 72 +++ .../java/feign/TrustingSSLSocketFactory.java | 99 ++++ .../feign/codec/DefaultErrorDecoderTest.java | 38 ++ .../feign/codec/RetryAfterDecoderTest.java | 46 ++ .../java/feign/examples/GitHubExample.java | 93 +++ .../test/java/feign/examples/IAMExample.java | 201 +++++++ gradle.properties | 2 +- settings.gradle | 4 +- 35 files changed, 3549 insertions(+), 27 deletions(-) create mode 100644 feign-core/src/main/java/feign/Client.java create mode 100644 feign-core/src/main/java/feign/Contract.java create mode 100644 feign-core/src/main/java/feign/Feign.java create mode 100644 feign-core/src/main/java/feign/FeignException.java create mode 100644 feign-core/src/main/java/feign/MethodHandler.java create mode 100644 feign-core/src/main/java/feign/MethodMetadata.java create mode 100644 feign-core/src/main/java/feign/ReflectiveFeign.java create mode 100644 feign-core/src/main/java/feign/Request.java create mode 100644 feign-core/src/main/java/feign/RequestTemplate.java create mode 100644 feign-core/src/main/java/feign/Response.java create mode 100644 feign-core/src/main/java/feign/RetryableException.java create mode 100644 feign-core/src/main/java/feign/Retryer.java create mode 100644 feign-core/src/main/java/feign/Target.java create mode 100644 feign-core/src/main/java/feign/Wire.java create mode 100644 feign-core/src/main/java/feign/codec/BodyEncoder.java create mode 100644 feign-core/src/main/java/feign/codec/Decoder.java create mode 100644 feign-core/src/main/java/feign/codec/Decoders.java create mode 100644 feign-core/src/main/java/feign/codec/ErrorDecoder.java create mode 100644 feign-core/src/main/java/feign/codec/FormEncoder.java create mode 100644 feign-core/src/main/java/feign/codec/SAXDecoder.java create mode 100644 feign-core/src/main/java/feign/codec/ToStringDecoder.java create mode 100644 feign-core/src/test/java/feign/ContractTest.java create mode 100644 feign-core/src/test/java/feign/DefaultRetryerTest.java create mode 100644 feign-core/src/test/java/feign/FeignTest.java create mode 100644 feign-core/src/test/java/feign/RequestTemplateTest.java create mode 100644 feign-core/src/test/java/feign/TrustingSSLSocketFactory.java create mode 100644 feign-core/src/test/java/feign/codec/DefaultErrorDecoderTest.java create mode 100644 feign-core/src/test/java/feign/codec/RetryAfterDecoderTest.java create mode 100644 feign-core/src/test/java/feign/examples/GitHubExample.java create mode 100644 feign-core/src/test/java/feign/examples/IAMExample.java diff --git a/.gitignore b/.gitignore index c82b5347c5..5b07c032e3 100644 --- a/.gitignore +++ b/.gitignore @@ -39,12 +39,16 @@ Thumbs.db # Gradle Files # ################ .gradle +local.properties # Build output directies /target -*/target -/build +**/test-output +**/target +**/bin +build */build +.m2 # IntelliJ specific files/directories out diff --git a/README.md b/README.md index ebf660a86f..0bf18dc73a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,100 @@ -feign -===== +# Feign makes writing java http clients easier +Feign is a java to http client binder inspired by [Dagger](https://github.com/square/dagger), [Retrofit](https://github.com/square/retrofit), [jclouds](https://github.com/jclouds/jclouds), and [JAXRS-2.0](https://jax-rs-spec.java.net/nonav/2.0/apidocs/index.html). Feign's first goal was reducing the complexity of binding [Denominator](https://github.com/Netflix/Denominator) uniformly to http apis regardless of [restfulness](http://www.slideshare.net/adrianfcole/99problems). + +### Why Feign and not X? + +You can use tools like Jersey and CXF to write java clients for ReST or SOAP services. You can write your own code on top of http transport libraries like Apache HC. Feign aims to connect your code to http apis with minimal overhead and code. Via customizable decoders and error handling, you should be able to write to any text-based http api. + +### How does Feign work? + +Feign works by processing annotations into a templatized request. Just before sending it off, arguments are applied to these templates in a straightforward fashion. While this limits Feign to only supporting text-based apis, it dramatically simplified system aspects such as replaying requests. It is also stupid easy to unit test your conversions knowing this. + +### Basics + +Usage typically looks like this, an adaptation of the [canonical Retrofit sample](https://github.com/square/retrofit/blob/master/retrofit-samples/github-client/src/main/java/com/example/retrofit/GitHubClient.java). + +```java +interface GitHub { + @GET @Path("/repos/{owner}/{repo}/contributors") List contributors(@PathParam("owner") String owner, @PathParam("repo") String repo); +} + +static class Contributor { + String login; + int contributions; +} + +public static void main(String... args) { + GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule()); + + // Fetch and print a list of the contributors to this library. + List contributors = github.contributors("netflix", "feign"); + for (Contributor contributor : contributors) { + System.out.println(contributor.login + " (" + contributor.contributions + ")"); + } +} +``` +### Decoders +The last argument to `Feign.create` specifies how to decode the responses. You can plug-in your favorite library, such as gson, or use builtin RegEx Pattern decoders. Here's how the Gson module looks. + +```java +@Module(overrides = true, library = true) +static class GsonModule { + @Provides @Singleton Map decoders() { + return ImmutableMap.of("GitHub", gsonDecoder); + } + + final Decoder gsonDecoder = new Decoder() { + Gson gson = new Gson(); + + @Override public Object decode(String methodKey, Reader reader, TypeToken type) { + return gson.fromJson(reader, type.getType()); + } + }; +} +``` +Feign doesn't offer a built-in json decoder as you can see above it is very few lines of code to wire yours in. If you are a jackson user, you'd probably thank us for not dragging in a dependency you don't use. + +### Multiple Interfaces +Feign can produce multiple api interfaces. These are defined as `Target` (default `HardCodedTarget`), which allow for dynamic discovery and decoration of requests prior to execution. + +For example, the following pattern might decorate each request with the current url and auth token from the identity service. + +```java +CloudDNS cloudDNS = Feign.create().newInstance(new CloudIdentityTarget(user, apiKey)); +``` + +You can find [several examples](https://github.com/Netflix/feign/tree/master/feign-core/src/test/java/feign/examples) in the test tree. Do take time to look at them, as seeing is believing! +### Advanced usage and Dagger +#### Dagger +Feign can be directly wired into Dagger which keeps things at compile time and Android friendly. As opposed to exposing builders for config, Feign intends users to embed their config in Dagger. + +Almost all configuration of Feign is represented as Map bindings, where the key is either the simple name (ex. `GitHub`) or the method (ex. `GitHub#contributors()`) in javadoc link format. For example, the following routes all decoding to gson: +```java +@Provides @Singleton Map decoders() { + return ImmutableMap.of("GitHub", gsonDecoder); +} +``` +#### Wire Logging +You can log the http messages going to and from the target by setting up a `Wire`. Here's the easiest way to do that: +```java +@Module(overrides = true) +class Overrides { + @Provides @Singleton Wire provideWire() { + return new Wire.LoggingWire().appendToFile("logs/http-wire.log"); + } +} +GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonGitHubModule(), new Overrides()); +``` +#### Pattern Decoders +If you have to only grab a single field from a server response, you may find regular expressions less maintenance than writing a type adapter. + +Here's how our IAM example grabs only one xml element from a response. +```java +@Module(overrides = true, library = true) +static class IAMModule { + @Provides @Singleton Map decoders() { + return ImmutableMap.of("IAM#arn()", Decoders.firstGroup("([\\S&&[^<]]+)")); + } +} +``` + diff --git a/build.gradle b/build.gradle index 5e6c63bb99..f99c728e51 100644 --- a/build.gradle +++ b/build.gradle @@ -23,29 +23,19 @@ apply from: file('gradle/release.gradle') subprojects { group = "com.netflix.${githubProjectName}" // TEMPLATE: Set to organization of project - - dependencies { - compile 'javax.ws.rs:jsr311-api:1.1.1' - compile 'com.sun.jersey:jersey-core:1.11' - testCompile 'org.testng:testng:6.1.1' - testCompile 'org.mockito:mockito-core:1.8.5' - } } -project(':template-client') { +project(':feign-core') { apply plugin: 'java' - dependencies { - compile 'org.slf4j:slf4j-api:1.6.3' - compile 'com.sun.jersey:jersey-client:1.11' - } -} -project(':template-server') { - apply plugin: 'war' - apply plugin: 'jetty' dependencies { - compile 'com.sun.jersey:jersey-server:1.11' - compile 'com.sun.jersey:jersey-servlet:1.11' - compile project(':template-client') - } + compile 'com.google.guava:guava:14.0.1' + compile 'com.squareup.dagger:dagger:1.0.1' + compile 'javax.ws.rs:jsr311-api:1.1.1' + provided 'com.squareup.dagger:dagger-compiler:1.0.1' + testCompile 'com.google.code.gson:gson:2.2.4' + testCompile 'com.fasterxml.jackson.core:jackson-databind:2.2.2' + testCompile 'org.testng:testng:6.8.1' + testCompile 'com.google.mockwebserver:mockwebserver:20130505' + } } diff --git a/feign-core/src/main/java/feign/Client.java b/feign-core/src/main/java/feign/Client.java new file mode 100644 index 0000000000..5659f283c3 --- /dev/null +++ b/feign-core/src/main/java/feign/Client.java @@ -0,0 +1,114 @@ +package feign; + +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.io.ByteSink; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.Reader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import javax.inject.Inject; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLSocketFactory; + +import dagger.Lazy; +import feign.Request.Options; + +import static com.google.common.base.Charsets.UTF_8; +import static com.google.common.net.HttpHeaders.CONTENT_LENGTH; + +/** + * Submits HTTP {@link Request requests}. Implementations are expected to be + * thread-safe. + */ +public interface Client { + /** + * Executes a request against its {@link Request#url() url} and returns a + * response. + * + * @param request safe to replay. + * @param options options to apply to this request. + * @return connected response, {@link Response.Body} is absent or unread. + * @throws IOException on a network error connecting to {@link Request#url()}. + */ + Response execute(Request request, Options options) throws IOException; + + public static class Default implements Client { + private final Lazy sslContextFactory; + + @Inject public Default(Lazy sslContextFactory) { + this.sslContextFactory = sslContextFactory; + } + + @Override public Response execute(Request request, Options options) throws IOException { + HttpURLConnection connection = convertAndSend(request, options); + return convertResponse(connection); + } + + HttpURLConnection convertAndSend(Request request, Options options) throws IOException { + final HttpURLConnection connection = (HttpURLConnection) new URL(request.url()).openConnection(); + if (connection instanceof HttpsURLConnection) { + HttpsURLConnection sslCon = (HttpsURLConnection) connection; + sslCon.setSSLSocketFactory(sslContextFactory.get()); + } + connection.setConnectTimeout(options.connectTimeoutMillis()); + connection.setReadTimeout(options.readTimeoutMillis()); + connection.setAllowUserInteraction(false); + connection.setInstanceFollowRedirects(true); + connection.setRequestMethod(request.method()); + + Integer contentLength = null; + for (Entry header : request.headers().entries()) { + if (header.getKey().equals(CONTENT_LENGTH)) + contentLength = Integer.valueOf(header.getValue()); + connection.addRequestProperty(header.getKey(), header.getValue()); + } + + if (request.body().isPresent()) { + if (contentLength != null) { + connection.setFixedLengthStreamingMode(contentLength); + } else { + connection.setChunkedStreamingMode(8196); + } + connection.setDoOutput(true); + new ByteSink() { + public OutputStream openStream() throws IOException { + return connection.getOutputStream(); + } + }.asCharSink(UTF_8).write(request.body().get()); + } + return connection; + } + + Response convertResponse(HttpURLConnection connection) throws IOException { + int status = connection.getResponseCode(); + String reason = connection.getResponseMessage(); + + ImmutableListMultimap.Builder headers = ImmutableListMultimap.builder(); + for (Map.Entry> field : connection.getHeaderFields().entrySet()) { + // response message + if (field.getKey() != null) + headers.putAll(field.getKey(), field.getValue()); + } + + Integer length = connection.getContentLength(); + if (length == -1) + length = null; + InputStream stream; + if (status >= 400) { + stream = connection.getErrorStream(); + } else { + stream = connection.getInputStream(); + } + Reader body = stream != null ? new InputStreamReader(stream) : null; + return Response.create(status, reason, headers.build(), body, length); + } + } +} diff --git a/feign-core/src/main/java/feign/Contract.java b/feign-core/src/main/java/feign/Contract.java new file mode 100644 index 0000000000..ab30935dbc --- /dev/null +++ b/feign-core/src/main/java/feign/Contract.java @@ -0,0 +1,120 @@ +package feign; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.reflect.TypeToken; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.net.URI; + +import javax.ws.rs.Consumes; +import javax.ws.rs.FormParam; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.HttpMethod; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.net.HttpHeaders.ACCEPT; +import static com.google.common.net.HttpHeaders.CONTENT_TYPE; + +/** + * Defines what annotations and values are valid on interfaces. + */ +public final class Contract { + + public static ImmutableSet parseAndValidatateMetadata(Class declaring) { + ImmutableSet.Builder builder = ImmutableSet.builder(); + for (Method method : declaring.getDeclaredMethods()) { + if (method.getDeclaringClass() == Object.class) + continue; + builder.add(parseAndValidatateMetadata(method)); + } + return builder.build(); + } + + public static MethodMetadata parseAndValidatateMetadata(Method method) { + MethodMetadata data = new MethodMetadata(); + data.returnType(TypeToken.of(method.getGenericReturnType())); + data.configKey(Feign.configKey(method)); + + for (Annotation methodAnnotation : method.getAnnotations()) { + Class annotationType = methodAnnotation.annotationType(); + HttpMethod http = annotationType.getAnnotation(HttpMethod.class); + if (http != null) { + checkState(data.template().method() == null, + "Method %s contains multiple HTTP methods. Found: %s and %s", method.getName(), data.template() + .method(), http.value()); + data.template().method(http.value()); + } else if (annotationType == RequestTemplate.Body.class) { + String body = RequestTemplate.Body.class.cast(methodAnnotation).value(); + if (body.indexOf('{') == -1) { + data.template().body(body); + } else { + data.template().bodyTemplate(body); + } + } else if (annotationType == Path.class) { + data.template().append(Path.class.cast(methodAnnotation).value()); + } else if (annotationType == Produces.class) { + data.template().header(CONTENT_TYPE, Joiner.on(',').join(((Produces) methodAnnotation).value())); + } else if (annotationType == Consumes.class) { + data.template().header(ACCEPT, Joiner.on(',').join(((Consumes) methodAnnotation).value())); + } + } + checkState(data.template().method() != null, "Method %s not annotated with HTTP method type (ex. GET, POST)", + method.getName()); + Class[] parameterTypes = method.getParameterTypes(); + + Annotation[][] parameterAnnotationArrays = method.getParameterAnnotations(); + int count = parameterAnnotationArrays.length; + for (int i = 0; i < count; i++) { + boolean hasHttpAnnotation = false; + + Class parameterType = parameterTypes[i]; + Annotation[] parameterAnnotations = parameterAnnotationArrays[i]; + if (parameterAnnotations != null) { + for (Annotation parameterAnnotation : parameterAnnotations) { + Class annotationType = parameterAnnotation.annotationType(); + if (annotationType == PathParam.class) { + data.indexToName().put(i, PathParam.class.cast(parameterAnnotation).value()); + hasHttpAnnotation = true; + } else if (annotationType == QueryParam.class) { + String name = QueryParam.class.cast(parameterAnnotation).value(); + data.template().query( + name, + ImmutableList.builder().addAll(data.template().queries().get(name)) + .add(String.format("{%s}", name)).build()); + data.indexToName().put(i, name); + hasHttpAnnotation = true; + } else if (annotationType == HeaderParam.class) { + String name = HeaderParam.class.cast(parameterAnnotation).value(); + data.template().header( + name, + ImmutableList.builder().addAll(data.template().headers().get(name)) + .add(String.format("{%s}", name)).build()); + data.indexToName().put(i, name); + hasHttpAnnotation = true; + } else if (annotationType == FormParam.class) { + String form = FormParam.class.cast(parameterAnnotation).value(); + data.formParams().add(form); + data.indexToName().put(i, form); + hasHttpAnnotation = true; + } + } + } + + if (parameterType == URI.class) { + data.urlIndex(i); + } else if (!hasHttpAnnotation) { + checkState(data.formParams().isEmpty(), "Body parameters cannot be used with @FormParam parameters."); + checkState(data.bodyIndex() == null, "Method has too many Body parameters: %s", method); + data.bodyIndex(i); + } + } + return data; + } +} \ No newline at end of file diff --git a/feign-core/src/main/java/feign/Feign.java b/feign-core/src/main/java/feign/Feign.java new file mode 100644 index 0000000000..d7a13cda63 --- /dev/null +++ b/feign-core/src/main/java/feign/Feign.java @@ -0,0 +1,148 @@ +package feign; + +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import java.lang.reflect.Method; +import java.util.Map; + +import javax.net.ssl.SSLSocketFactory; + +import dagger.ObjectGraph; +import dagger.Provides; +import feign.Request.Options; +import feign.Target.HardCodedTarget; +import feign.Wire.NoOpWire; +import feign.codec.BodyEncoder; +import feign.codec.Decoder; +import feign.codec.ErrorDecoder; +import feign.codec.FormEncoder; + +/** + * Feign's purpose is to ease development against http apis that feign + * restfulness. + *

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

+ * For example. + *

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

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

+ *

Note

+ *

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

+ *

relationship to JAXRS 2.0

+ *

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

+ * ex. + *

+ *

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

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

+ *

+ * For example: + *

+ *

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

+ *

relationship to JAXRS 2.0

+ *

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

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

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

+ *

relationship to JAXRS 2.0

+ *

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

+ * ex. + *

+ *

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

+ *

relationship to JAXRS 2.0

+ *

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

+ * ex. + *

+ *

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

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

+ *

relationship to JAXRS 2.0

+ *

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

+ * ex. + *

+ *

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

+ *

relationship to JAXRS 2.0

+ *

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

+ * ex. + *

+ *

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

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

+ *

Note

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

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

relationship to JAXRS 2.0

+ *

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

+ *

+ * For example: + *

+ *

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

+ *

relationship to JAXRS 2.0

+ *

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

+ * Ex. + *

+ *

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

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

+ *

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

+ * Ex. + *

+ *

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

+ *

Error handling

+ *

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

+ *

Pattern Decoders

+ *

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

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

+ *

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

+ *

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

+ *

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

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

+ *

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

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

+ *

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

+ * Ex. + *

+ *

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

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

+ *

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

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

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

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

*

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

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

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

diff --git a/CHANGES.md b/CHANGES.md
new file mode 100644
index 0000000000..0541be1724
--- /dev/null
+++ b/CHANGES.md
@@ -0,0 +1,6 @@
+### Version 1.1.0
+* adds Ribbon integration
+
+### Version 1.0.0
+
+* Initial open source release
diff --git a/README.md b/README.md
index 8a6c81865f..a370808e7e 100644
--- a/README.md
+++ b/README.md
@@ -65,6 +65,16 @@ CloudDNS cloudDNS =  Feign.create().newInstance(new CloudIdentityTarget {
+
+  private final Client delegate;
+  private final int connectTimeout;
+  private final int readTimeout;
+
+  LBClient(Client delegate, ILoadBalancer lb, IClientConfig clientConfig) {
+    this.delegate = delegate;
+    this.connectTimeout = Integer.valueOf(clientConfig.getProperty(ConnectTimeout).toString());
+    this.readTimeout = Integer.valueOf(clientConfig.getProperty(ReadTimeout).toString());
+    setLoadBalancer(lb);
+    initWithNiwsConfig(clientConfig);
+  }
+
+  @Override
+  public RibbonResponse execute(RibbonRequest request) throws IOException {
+    int connectTimeout = config(request, ConnectTimeout, this.connectTimeout);
+    int readTimeout = config(request, ReadTimeout, this.readTimeout);
+
+    Request.Options options = new Request.Options(connectTimeout, readTimeout);
+    Response response = delegate.execute(request.toRequest(), options);
+    return new RibbonResponse(request.getUri(), response);
+  }
+
+  @Override protected boolean isCircuitBreakerException(Exception e) {
+    return e instanceof IOException;
+  }
+
+  @Override protected boolean isRetriableException(Exception e) {
+    return e instanceof RetryableException;
+  }
+
+  @Override
+  protected Pair deriveSchemeAndPortFromPartialUri(RibbonRequest task) {
+    return new Pair(URI.create(task.request.url()).getScheme(), task.getUri().getPort());
+  }
+
+  @Override protected int getDefaultPort() {
+    return 443;
+  }
+
+  static class RibbonRequest extends ClientRequest implements Cloneable {
+
+    private final Request request;
+
+    RibbonRequest(Request request, URI uri) {
+      this.request = request;
+      setUri(uri);
+    }
+
+    Request toRequest() {
+      return new RequestTemplate()
+          .method(request.method())
+          .append(getUri().toASCIIString())
+          .headers(request.headers())
+          .body(request.body().orNull()).request();
+    }
+
+    public Object clone() {
+      return new RibbonRequest(request, getUri());
+    }
+  }
+
+  static class RibbonResponse implements IResponse {
+
+    private final URI uri;
+    private final Response response;
+
+    RibbonResponse(URI uri, Response response) {
+      this.uri = uri;
+      this.response = response;
+    }
+
+    @Override public Object getPayload() throws ClientException {
+      return response.body().orNull();
+    }
+
+    @Override public boolean hasPayload() {
+      return response.body().isPresent();
+    }
+
+    @Override public boolean isSuccess() {
+      return response.status() == 200;
+    }
+
+    @Override public URI getRequestedURI() {
+      return uri;
+    }
+
+    @Override public Map> getHeaders() {
+      return response.headers().asMap();
+    }
+
+    Response toResponse() {
+      return response;
+    }
+  }
+
+  static int config(RibbonRequest request, CommonClientConfigKey key, int defaultValue) {
+    if (request.getOverrideConfig() != null && request.getOverrideConfig().containsProperty(key))
+      return Integer.valueOf(request.getOverrideConfig().getProperty(key).toString());
+    return defaultValue;
+  }
+}
diff --git a/feign-ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java b/feign-ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java
new file mode 100644
index 0000000000..337793ff2c
--- /dev/null
+++ b/feign-ribbon/src/main/java/feign/ribbon/LoadBalancingTarget.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2013 Netflix, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package feign.ribbon;
+
+import com.google.common.base.Objects;
+import com.netflix.loadbalancer.AbstractLoadBalancer;
+import com.netflix.loadbalancer.Server;
+
+import java.net.URI;
+
+import feign.Request;
+import feign.RequestTemplate;
+import feign.Target;
+
+import static com.google.common.base.Objects.equal;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.netflix.client.ClientFactory.getNamedLoadBalancer;
+import static java.lang.String.format;
+
+/**
+ * Basic integration for {@link com.netflix.loadbalancer.ILoadBalancer loadbalancer-aware} targets.
+ * Using this will enable dynamic url discovery via ribbon including incrementing server request counts.
+ * 

+ * Ex. + *

+ * MyService api = Feign.create(LoadBalancingTarget.create(MyService.class, "http://myAppProd"))
+ * 
+ * Where {@code myAppProd} is the ribbon loadbalancer name and {@code myAppProd.ribbon.listOfServers} configuration + * is set. + * + * @param corresponds to {@link feign.Target#type()} + */ +public class LoadBalancingTarget implements Target { + + /** + * creates a target which dynamically derives urls from a {@link com.netflix.loadbalancer.ILoadBalancer loadbalancer}. + * + * @param type corresponds to {@link feign.Target#type()} + * @param schemeName naming convention is {@code https://name} or {@code http://name} where + * name corresponds to {@link com.netflix.client.ClientFactory#getNamedLoadBalancer(String)} + */ + public static LoadBalancingTarget create(Class type, String schemeName) { + URI asUri = URI.create(schemeName); + return new LoadBalancingTarget(type, asUri.getScheme(), asUri.getHost()); + } + + private final String name; + private final String scheme; + private final Class type; + private final AbstractLoadBalancer lb; + + protected LoadBalancingTarget(Class type, String scheme, String name) { + this.type = checkNotNull(type, "type"); + this.scheme = checkNotNull(scheme, "scheme"); + this.name = checkNotNull(name, "name"); + this.lb = AbstractLoadBalancer.class.cast(getNamedLoadBalancer(name())); + } + + @Override public Class type() { + return type; + } + + @Override public String name() { + return name; + } + + @Override public String url() { + return name; + } + + /** + * current load balancer for the target. + */ + public AbstractLoadBalancer lb() { + return lb; + } + + @Override public Request apply(RequestTemplate input) { + Server currentServer = lb.chooseServer(null); + String url = format("%s://%s", scheme, currentServer.getHostPort()); + input.insert(0, url); + try { + return input.request(); + } finally { + lb.getLoadBalancerStats().incrementNumRequests(currentServer); + } + } + + @Override public int hashCode() { + return Objects.hashCode(type, name); + } + + @Override public boolean equals(Object obj) { + if (this == obj) + return true; + if (LoadBalancingTarget.class != obj.getClass()) + return false; + LoadBalancingTarget that = LoadBalancingTarget.class.cast(obj); + return equal(this.type, that.type) && equal(this.name, that.name); + } +} diff --git a/feign-ribbon/src/main/java/feign/ribbon/RibbonModule.java b/feign-ribbon/src/main/java/feign/ribbon/RibbonModule.java new file mode 100644 index 0000000000..aadc0d68e6 --- /dev/null +++ b/feign-ribbon/src/main/java/feign/ribbon/RibbonModule.java @@ -0,0 +1,89 @@ +/* + * Copyright 2013 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign.ribbon; + +import com.google.common.base.Throwables; +import com.netflix.client.ClientException; +import com.netflix.client.ClientFactory; +import com.netflix.client.config.IClientConfig; +import com.netflix.loadbalancer.ILoadBalancer; + +import java.io.IOException; +import java.net.URI; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import dagger.Provides; +import feign.Client; +import feign.Request; +import feign.Response; + +/** + * Adding this module will override URL resolution of {@link feign.Client Feign's client}, + * adding smart routing and resiliency capabilities provided by Ribbon. + *

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

+ * Ex. + *

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

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

*

Note

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

relationship to JAXRS 2.0

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

*

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

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

*

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

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

*

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

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

+ * ex. + *

+ *

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

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

+ *

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

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

Relationship to JAXRS

+ *

+ * The following two forms are identical. + *

+ * Feign: + *

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

+ * JAX-RS: + *

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

+ *

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

+ *

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

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

Relationship to JAXRS

+ *

+ * The following two forms are identical. + *

+ * Feign: + *

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

+ * JAX-RS: + *

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

- * ex. - *

- *

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

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

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

*

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

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

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

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

+ *
* ex. - *

+ *
*

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

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

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

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

+ *
* For example. *

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

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

+ *
*

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

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

Relationship to JAXRS

- *

+ *

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

+ *
* Feign: *

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

+ *
* JAX-RS: *

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

- *

Note

- *

+ *
+ *

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

+ *
*

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

+ *
*

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

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

Relationship to JAXRS

- *

+ *

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

+ *
* Feign: *

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

+ *
* JAX-RS: *

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

- *

relationship to JAXRS 2.0

- *

+ *
+ *

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

- *

+ *
+ *
* For example: - *

+ *
*

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

- *

relationship to JAXRS 2.0

- *

+ *
+ *

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

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

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

- *

relationship to JAXRS 2.0

- *

+ *
+ *

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

+ *
* ex. - *

+ *
*

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

- *

relationship to JAXRS 2.0

- *

+ *
+ *

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

+ *
* ex. - *

+ *
*

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

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

- *

relationship to JAXRS 2.0

- *

+ *
+ *

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

+ *
* ex. - *

+ *
*

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

- *

relationship to JAXRS 2.0

- *

+ *
+ *

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

+ *
* ex. - *

+ *
*

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

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

- *

Note

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

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

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

relationship to JAXRS 2.0

- *

+ *

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

- *

+ *
+ *
* For example: - *

+ *
*

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

- *

relationship to JAXRS 2.0

- *

+ *
+ *

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

+ *
* Ex. - *

+ *
*

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

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

+ *
*

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

+ *
* Ex. - *

+ *
*

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

- *

Error handling

- *

+ *
+ *

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

- *

Pattern Decoders

- *

+ *
+ *

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

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

+ *
*

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

- *

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

+ *
*

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

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

+ *
*

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

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

+ *
*

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

+ *
* Ex. - *

+ *
*

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

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

+ *
*

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

+ *
* Ex. *

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

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

+ *
* Ex. *

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

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

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

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

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

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

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

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

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

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

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

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

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

+ * Implementation copied from {@code retrofit.RestMethodInfo}. + * + * @param genericContext Ex. {@link java.lang.reflect.Field#getGenericType()} + * @param supertype Ex. {@code Decoder.class} + * @return in the example above, the type parameter of {@code Decoder}. + * @throws IllegalStateException if {@code supertype} cannot be resolved into a parameterized type using + * {@code context}. + */ + public static Type resolveLastTypeParameter(Type genericContext, Class supertype) throws IllegalStateException { + Type resolvedSuperType = Types.getSupertype(genericContext, Types.getRawType(genericContext), supertype); + checkState(resolvedSuperType instanceof ParameterizedType, "could not resolve %s into a parameterized type %s", + genericContext, supertype); + Type[] types = ParameterizedType.class.cast(resolvedSuperType).getActualTypeArguments(); + for (int i = 0; i < types.length; i++) { + Type type = types[i]; + if (type instanceof WildcardType) { + types[i] = ((WildcardType) type).getUpperBounds()[0]; + } + } + return types[types.length - 1]; + } } diff --git a/feign-core/src/main/java/feign/codec/BodyEncoder.java b/feign-core/src/main/java/feign/codec/BodyEncoder.java deleted file mode 100644 index 74f7c026eb..0000000000 --- a/feign-core/src/main/java/feign/codec/BodyEncoder.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package feign.codec; - -import feign.RequestTemplate; - -public interface BodyEncoder { - /** - * Converts objects to an appropriate representation. Can affect any part of - * {@link RequestTemplate}. - *
- * Ex. - *
- *

-   * public class GsonEncoder implements BodyEncoder {
-   *   private final Gson gson;
-   *
-   *   public GsonEncoder(Gson gson) {
-   *     this.gson = gson;
-   *   }
-   *
-   *   @Override
-   *   public void encodeBody(Object bodyParam, RequestTemplate base) {
-   *     base.body(gson.toJson(bodyParam));
-   *   }
-   * }
-   * 
- *
- * If a parameter has no {@code *Param} annotation, it is passed to this - * method. - *
- *
-   * @POST
-   * @Path("/")
-   * void create(User user);
-   * 
- * - * @param bodyParam a body parameter - * @param base template to encode the {@code object} into. - */ - void encodeBody(Object bodyParam, RequestTemplate base); -} diff --git a/feign-core/src/main/java/feign/codec/DecodeException.java b/feign-core/src/main/java/feign/codec/DecodeException.java new file mode 100644 index 0000000000..5efab25ba6 --- /dev/null +++ b/feign-core/src/main/java/feign/codec/DecodeException.java @@ -0,0 +1,45 @@ +/* + * Copyright 2013 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign.codec; + +import feign.FeignException; + +import static feign.Util.checkNotNull; + +/** + * Similar to {@code javax.websocket.DecodeException}, raised when a problem + * occurs decoding a message. Note that {@code DecodeException} is not an + * {@code IOException}, nor have one set as its cause. + */ +public class DecodeException extends FeignException { + + /** + * @param message the reason for the failure. + */ + public DecodeException(String message) { + super(checkNotNull(message, "message")); + } + + /** + * @param message the reason for the failure. + * @param cause the cause of the error. + */ + public DecodeException(String message, Throwable cause) { + super(checkNotNull(message, "message"), checkNotNull(cause, "cause")); + } + + private static final long serialVersionUID = 1L; +} diff --git a/feign-core/src/main/java/feign/codec/Decoder.java b/feign-core/src/main/java/feign/codec/Decoder.java index 98cefdaa91..afcc6406ee 100644 --- a/feign-core/src/main/java/feign/codec/Decoder.java +++ b/feign-core/src/main/java/feign/codec/Decoder.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -19,32 +19,17 @@ import java.io.Reader; import java.lang.reflect.Type; +import feign.FeignException; import feign.Response; -import static feign.Util.ensureClosed; - /** * Decodes an HTTP response into a given type. Invoked when - * {@link Response#status()} is in the 2xx range. - *
- * Ex. + * {@link Response#status()} is in the 2xx range. Like + * {@code javax.websocket.Decoder}, except that the decode method is passed the + * generic type of the target.
*
- *
- * public class GsonDecoder extends Decoder {
- *   private final Gson gson;
- *
- *   public GsonDecoder(Gson gson) {
- *     this.gson = gson;
- *   }
- *
- *   @Override
- *   public Object decode(Reader reader, Type type) {
- *     return gson.fromJson(reader, type);
- *   }
- * }
- * 
*
- *

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

+ *

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

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

*

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

*

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

*

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

+ *
+ *

+ *

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

*
+ *

*

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

+ *
+ *

+ *

+     * new TransformEachFirstGroup<Foo>(pattern, applyFirstGroup) {
+     * }; // note the curly braces ensures no type erasure!
+     * 
+ */ + protected TransformEachFirstGroup(String pattern, ApplyFirstGroup applyFirstGroup) { + this.patternForMatcher = compile(checkNotNull(pattern, "pattern"), DOTALL); + this.applyFirstGroup = checkNotNull(applyFirstGroup, "applyFirstGroup"); + } - private static final StringDecoder TO_STRING = new StringDecoder(); + @Override + public List decode(Reader reader, Type type) throws IOException { + Matcher matcher = patternForMatcher.matcher(Decoders.toString(reader)); + List result = new ArrayList(); + while (matcher.find()) { + result.add(applyFirstGroup.apply(matcher.group(1))); + } + return result; + } - private static final ApplyFirstGroup IDENTITY = new ApplyFirstGroup() { - @Override public String apply(String firstGroup) { - return firstGroup; + @Override + public String toString() { + return format("decode %s into list elements, where each group(1) is transformed with %s", + patternForMatcher, applyFirstGroup); } - }; + } } diff --git a/feign-core/src/main/java/feign/codec/EncodeException.java b/feign-core/src/main/java/feign/codec/EncodeException.java new file mode 100644 index 0000000000..12d06ba340 --- /dev/null +++ b/feign-core/src/main/java/feign/codec/EncodeException.java @@ -0,0 +1,45 @@ +/* + * Copyright 2013 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign.codec; + +import feign.FeignException; + +import static feign.Util.checkNotNull; + +/** + * Similar to {@code javax.websocket.EncodeException}, raised when a problem + * occurs decoding a message. Note that {@code DecodeException} is not an + * {@code IOException}, nor have one set as its cause. + */ +public class EncodeException extends FeignException { + + /** + * @param message the reason for the failure. + */ + public EncodeException(String message) { + super(checkNotNull(message, "message")); + } + + /** + * @param message the reason for the failure. + * @param cause the cause of the error. + */ + public EncodeException(String message, Throwable cause) { + super(checkNotNull(message, "message"), checkNotNull(cause, "cause")); + } + + private static final long serialVersionUID = 1L; +} diff --git a/feign-core/src/main/java/feign/codec/Encoder.java b/feign-core/src/main/java/feign/codec/Encoder.java new file mode 100644 index 0000000000..80614b53fd --- /dev/null +++ b/feign-core/src/main/java/feign/codec/Encoder.java @@ -0,0 +1,75 @@ +/* + * Copyright 2013 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feign.codec; + +/** + * Encodes an object into an HTTP request body. Like + * {@code javax.websocket.Encoder}.
+ * {@code Encoder} is used when a method parameter has no {@code *Param} + * annotation. For example:
+ *

+ *

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

+ *

Form encoding

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

+ *

+   * public class GsonEncoder implements Encoder.Text<Object> {
+   *     private final Gson gson;
+   *
+   *     public GsonEncoder(Gson gson) {
+   *         this.gson = gson;
+   *     }
+   *
+   *     @Override
+   *     public String encode(Object object) {
+   *         return gson.toJson(object);
+   *     }
+   * }
+   * 
+ */ + interface Text extends Encoder { + /** + * Implement this to encode an object as a String.. If you need to wrap + * exceptions, please do so via {@link EncodeException} + * + * @param object what to encode as the request body. + * @return the encoded object as a string. * @throws EncodeException + * when encoding failed due to a checked exception. + */ + String encode(T object) throws EncodeException; + } +} diff --git a/feign-core/src/main/java/feign/codec/ErrorDecoder.java b/feign-core/src/main/java/feign/codec/ErrorDecoder.java index 169935042a..d9982360e5 100644 --- a/feign-core/src/main/java/feign/codec/ErrorDecoder.java +++ b/feign-core/src/main/java/feign/codec/ErrorDecoder.java @@ -68,7 +68,7 @@ public interface ErrorDecoder { */ public Exception decode(String methodKey, Response response); - public static final ErrorDecoder DEFAULT = new ErrorDecoder() { + public static class Default implements ErrorDecoder { private final RetryAfterDecoder retryAfterDecoder = new RetryAfterDecoder(); @@ -87,7 +87,7 @@ private T firstOrNull(Map> map, String key) { } return null; } - }; + } /** * Decodes a {@link feign.Util#RETRY_AFTER} header into an absolute date, diff --git a/feign-core/src/main/java/feign/codec/FormEncoder.java b/feign-core/src/main/java/feign/codec/FormEncoder.java deleted file mode 100644 index 381f80c2d8..0000000000 --- a/feign-core/src/main/java/feign/codec/FormEncoder.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package feign.codec; - -import java.util.Map; - -import feign.RequestTemplate; - -public interface FormEncoder { - - /** - * FormParam encoding - *
- * If any parameters are found in {@link feign.MethodMetadata#formParams()}, they will be - * collected and passed as {code formParams} - *
- *
-   * @POST
-   * @Path("/")
-   * Session login(@FormParam("username") String username, @FormParam("password") String password);
-   * 
- * - * @param formParams Object instance to convert. - * @param base template to encode the {@code object} into. - */ - void encodeForm(Map formParams, RequestTemplate base); -} diff --git a/feign-core/src/main/java/feign/codec/SAXDecoder.java b/feign-core/src/main/java/feign/codec/SAXDecoder.java index 25cf8efc9c..972fee9cc2 100644 --- a/feign-core/src/main/java/feign/codec/SAXDecoder.java +++ b/feign-core/src/main/java/feign/codec/SAXDecoder.java @@ -19,46 +19,56 @@ import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; +import org.xml.sax.helpers.XMLReaderFactory; +import javax.inject.Provider; import java.io.IOException; import java.io.Reader; import java.lang.reflect.Type; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.parsers.SAXParserFactory; - import static feign.Util.checkNotNull; import static feign.Util.checkState; -public abstract class SAXDecoder extends Decoder { +public class SAXDecoder implements Decoder.TextStream { /* Implementations are not intended to be shared across requests. */ - public interface ContentHandlerWithResult extends ContentHandler { - /* expected to be set following a call to {@link XMLReader#parse(InputSource)} */ - Object getResult(); + public interface ContentHandlerWithResult extends ContentHandler { + /* + * expected to be set following a call to {@link + * XMLReader#parse(InputSource)} + */ + T result(); } - private final SAXParserFactory factory; - - protected SAXDecoder() { - this(SAXParserFactory.newInstance()); - factory.setNamespaceAware(false); - factory.setValidating(false); - } + private final Provider> handlers; - protected SAXDecoder(SAXParserFactory factory) { - this.factory = checkNotNull(factory, "factory"); + /** + * You must subclass this, in order to prevent type erasure on {@code T}. In + * addition to making a concrete type, you can also use the following form. + *

+ *
+ *

+ *

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

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

+ *

+   * public class GsonDecoder implements Decoder.TextStream<Object> {
+   *   private final Gson gson;
+   *
+   *   public GsonDecoder(Gson gson) {
+   *     this.gson = gson;
+   *   }
+   *
+   *   @Override
+   *   public Object decode(Reader reader, Type type) throws IOException {
+   *     try {
+   *       return gson.fromJson(reader, type);
+   *     } catch (JsonIOException e) {
+   *       if (e.getCause() != null &&
+   *           e.getCause() instanceof IOException) {
+   *         throw IOException.class.cast(e.getCause());
+   *       }
+   *       throw e;
+   *     }
+   *   }
+   * }
+   * 
+ *
+   * public class GsonIncrementalDecoder implements IncrementalDecoder {
+   *   private final Gson gson;
+   *
+   *   public GsonIncrementalDecoder(Gson gson) {
+   *     this.gson = gson;
+   *   }
+   *
+   *   @Override public void decode(Reader reader, Type type, IncrementalCallback incrementalCallback) throws Exception {
+   *     JsonReader jsonReader = new JsonReader(reader);
+   *     jsonReader.beginArray();
+   *     while (jsonReader.hasNext()) {
+   *       try {
+   *          incrementalCallback.onNext(gson.fromJson(jsonReader, type));
+   *       } catch (JsonIOException e) {
+   *         if (e.getCause() != null &&
+   *             e.getCause() instanceof IOException) {
+   *           throw IOException.class.cast(e.getCause());
+   *         }
+   *         throw e;
+   *       }
+   *     }
+   *     jsonReader.endArray();
+   *   }
+   * }
+   * 
+   */
+  public interface TextStream extends IncrementalDecoder {
+  }
+}
diff --git a/feign-core/src/main/java/feign/codec/StringIncrementalDecoder.java b/feign-core/src/main/java/feign/codec/StringIncrementalDecoder.java
new file mode 100644
index 0000000000..3e9dc8e005
--- /dev/null
+++ b/feign-core/src/main/java/feign/codec/StringIncrementalDecoder.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2013 Netflix, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package feign.codec;
+
+import feign.IncrementalCallback;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.lang.reflect.Type;
+
+public class StringIncrementalDecoder implements IncrementalDecoder.TextStream {
+  private static final StringDecoder STRING_DECODER = new StringDecoder();
+
+  @Override
+  public void decode(Reader reader, Type type, IncrementalCallback incrementalCallback)
+      throws IOException {
+    incrementalCallback.onNext(STRING_DECODER.decode(reader, type));
+  }
+}
diff --git a/feign-core/src/test/java/feign/FeignTest.java b/feign-core/src/test/java/feign/FeignTest.java
index 3155e1549f..ac91708ba7 100644
--- a/feign-core/src/test/java/feign/FeignTest.java
+++ b/feign-core/src/test/java/feign/FeignTest.java
@@ -19,10 +19,10 @@
 import com.google.mockwebserver.MockResponse;
 import com.google.mockwebserver.MockWebServer;
 import com.google.mockwebserver.SocketPolicy;
+import dagger.Lazy;
 import dagger.Module;
 import dagger.Provides;
 import feign.codec.Decoder;
-import feign.codec.EncodeException;
 import feign.codec.Encoder;
 import feign.codec.ErrorDecoder;
 import feign.codec.StringDecoder;
@@ -38,14 +38,35 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import static dagger.Provides.Type.SET;
 import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
 
 @Test
 // unbound wildcards are not currently injectable in dagger.
 @SuppressWarnings("rawtypes")
 public class FeignTest {
+
+  @Test public void closeShutsdownExecutorService() throws IOException, InterruptedException {
+    final ExecutorService service = Executors.newCachedThreadPool();
+    new Feign(new Lazy() {
+      @Override public Executor get() {
+        return service;
+      }
+    }) {
+      @Override public  T newInstance(Target target) {
+        return null;
+      }
+    }.close();
+    assertTrue(service.isShutdown());
+  }
+
   interface TestInterface {
     @RequestLine("POST /") String post();
 
@@ -54,15 +75,19 @@ interface TestInterface {
     void login(
         @Named("customer_name") String customer, @Named("user_name") String user, @Named("password") String password);
 
-    @RequestLine("POST /")
-    void body(List contents);
+    @RequestLine("POST /") void body(List contents);
 
-    @RequestLine("POST /")
-    void form(
+    @RequestLine("POST /") void form(
         @Named("customer_name") String customer, @Named("user_name") String user, @Named("password") String password);
 
     @RequestLine("GET /{1}/{2}") Response uriParam(@Named("1") String one, URI endpoint, @Named("2") String two);
 
+    @RequestLine("POST /") void incrementVoid(IncrementalCallback incrementalCallback);
+
+    @RequestLine("POST /") void incrementString(IncrementalCallback incrementalCallback);
+
+    @RequestLine("POST /") void incrementResponse(IncrementalCallback incrementalCallback);
+
     @dagger.Module(overrides = true, library = true)
     static class Module {
       @Provides(type = SET) Encoder defaultEncoder() {
@@ -80,6 +105,117 @@ static class Module {
           }
         };
       }
+
+      // just run synchronously
+      @Provides @Singleton @Named("http") Executor httpExecutor() {
+        return new Executor() {
+          @Override public void execute(Runnable command) {
+            command.run();
+          }
+        };
+      }
+    }
+  }
+
+  @Test
+  public void incrementVoid() throws IOException, InterruptedException {
+    final MockWebServer server = new MockWebServer();
+    server.enqueue(new MockResponse().setResponseCode(200).setBody("foo"));
+    server.play();
+
+    try {
+      TestInterface api = Feign.create(TestInterface.class, server.getUrl("").toString(), new TestInterface.Module());
+
+      final AtomicBoolean success = new AtomicBoolean();
+
+      IncrementalCallback incrementalCallback = new IncrementalCallback() {
+
+        @Override public void onNext(Void element) {
+          fail("on next isn't valid for void");
+        }
+
+        @Override public void onSuccess() {
+          success.set(true);
+        }
+
+        @Override public void onFailure(Throwable cause) {
+          fail(cause.getMessage());
+        }
+      };
+      api.incrementVoid(incrementalCallback);
+
+      assertTrue(success.get());
+      assertEquals(server.getRequestCount(), 1);
+    } finally {
+      server.shutdown();
+    }
+  }
+
+  @Test
+  public void incrementResponse() throws IOException, InterruptedException {
+    final MockWebServer server = new MockWebServer();
+    server.enqueue(new MockResponse().setResponseCode(200).setBody("foo"));
+    server.play();
+
+    try {
+      TestInterface api = Feign.create(TestInterface.class, server.getUrl("").toString(), new TestInterface.Module());
+
+      final AtomicBoolean success = new AtomicBoolean();
+
+      IncrementalCallback incrementalCallback = new IncrementalCallback() {
+
+        @Override public void onNext(Response element) {
+          assertEquals(element.status(), 200);
+        }
+
+        @Override public void onSuccess() {
+          success.set(true);
+        }
+
+        @Override public void onFailure(Throwable cause) {
+          fail(cause.getMessage());
+        }
+      };
+      api.incrementResponse(incrementalCallback);
+
+      assertTrue(success.get());
+      assertEquals(server.getRequestCount(), 1);
+    } finally {
+      server.shutdown();
+    }
+  }
+
+  @Test
+  public void incrementString() throws IOException, InterruptedException {
+    final MockWebServer server = new MockWebServer();
+    server.enqueue(new MockResponse().setResponseCode(200).setBody("foo"));
+    server.play();
+
+    try {
+      TestInterface api = Feign.create(TestInterface.class, server.getUrl("").toString(), new TestInterface.Module());
+
+      final AtomicBoolean success = new AtomicBoolean();
+
+      IncrementalCallback incrementalCallback = new IncrementalCallback() {
+
+        @Override public void onNext(String element) {
+          assertEquals(element, "foo");
+        }
+
+        @Override public void onSuccess() {
+          success.set(true);
+        }
+
+        @Override public void onFailure(Throwable cause) {
+          fail(cause.getMessage());
+        }
+      };
+      api.incrementString(incrementalCallback);
+
+      assertTrue(success.get());
+      assertEquals(server.getRequestCount(), 1);
+    } finally {
+      server.shutdown();
     }
   }
 

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

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

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

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

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

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

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

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

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

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

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

diff --git a/examples/feign-example-cli/build.gradle b/examples/feign-example-cli/build.gradle
index 1a5882372d..55b0af2dea 100644
--- a/examples/feign-example-cli/build.gradle
+++ b/examples/feign-example-cli/build.gradle
@@ -1,8 +1,8 @@
 apply plugin: 'java'
 
 dependencies {
-  compile  'com.netflix.feign:feign-core:2.0.0'
-  compile  'com.google.code.gson:gson:2.2.4'
+  compile  'com.netflix.feign:feign-core:3.0.0'
+  compile  'com.netflix.feign:feign-gson:3.0.0'
   provided 'com.squareup.dagger:dagger-compiler:1.0.1'
 }
 
diff --git a/examples/feign-example-cli/src/main/java/feign/example/cli/GitHubExample.java b/examples/feign-example-cli/src/main/java/feign/example/cli/GitHubExample.java
index 48597d7e54..3106e5116a 100644
--- a/examples/feign-example-cli/src/main/java/feign/example/cli/GitHubExample.java
+++ b/examples/feign-example-cli/src/main/java/feign/example/cli/GitHubExample.java
@@ -15,22 +15,14 @@
  */
 package feign.example.cli;
 
-import com.google.gson.Gson;
-
-import java.io.Reader;
-import java.lang.reflect.Type;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-import javax.inject.Named;
-import javax.inject.Singleton;
-
-import dagger.Module;
-import dagger.Provides;
 import feign.Feign;
+import feign.IncrementalCallback;
 import feign.RequestLine;
-import feign.codec.Decoder;
+import feign.gson.GsonModule;
+
+import javax.inject.Named;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
 
 /**
  * adapted from {@code com.example.retrofit.GitHubClient}
@@ -40,6 +32,10 @@ public class GitHubExample {
   interface GitHub {
     @RequestLine("GET /repos/{owner}/{repo}/contributors")
     List contributors(@Named("owner") String owner, @Named("repo") String repo);
+
+    @RequestLine("GET /repos/{owner}/{repo}/contributors")
+    void contributors(@Named("owner") String owner, @Named("repo") String repo,
+                      IncrementalCallback contributors);
   }
 
   static class Contributor {
@@ -47,33 +43,45 @@ static class Contributor {
     int contributions;
   }
 
-  public static void main(String... args) {
+  public static void main(String... args) throws InterruptedException {
     GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule());
 
-    // Fetch and print a list of the contributors to this library.
+    System.out.println("Let's fetch and print a list of the contributors to this library.");
     List contributors = github.contributors("netflix", "feign");
     for (Contributor contributor : contributors) {
       System.out.println(contributor.login + " (" + contributor.contributions + ")");
     }
-  }
 
-  /**
-   * Here's how to wire gson deserialization.
-   */
-  @Module(overrides = true, library = true)
-  static class GsonModule {
-    @Provides @Singleton Map decoders() {
-      Map decoders = new LinkedHashMap();
-      decoders.put("GitHub", jsonDecoder);
-      return decoders;
-    }
+    final CountDownLatch latch = new CountDownLatch(1);
+
+    System.out.println("Now, let's do it as an incremental async task.");
+    IncrementalCallback task = new IncrementalCallback() {
+
+      public int count;
+
+      // parsed directly from the text stream without an intermediate collection.
+      @Override public void onNext(Contributor contributor) {
+        System.out.println(contributor.login + " (" + contributor.contributions + ")");
+        count++;
+      }
 
-    final Decoder jsonDecoder = new Decoder() {
-      Gson gson = new Gson();
+      @Override public void onSuccess() {
+        System.out.println("found " + count + " contributors");
+        latch.countDown();
+      }
 
-      @Override public Object decode(String methodKey, Reader reader, Type type) {
-        return gson.fromJson(reader, type);
+      @Override public void onFailure(Throwable cause) {
+        cause.printStackTrace();
+        latch.countDown();
       }
     };
+
+    // fire a task in the background.
+    github.contributors("netflix", "feign", task);
+
+    // wait for the task to complete.
+    latch.await();
+
+    System.exit(0);
   }
 }

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

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

diff --git a/CHANGES.md b/CHANGES.md
index 30ab975ce9..4fb4714af8 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,9 @@
+### Version 4.0
+* Support RxJava-style Observers.
+  * Return type can be `Observable` for an async equiv of `Iterable`.
+  * `Observer` replaces `IncrementalCallback` and is passed to `Observable.subscribe()`.
+  * On `Subscription.unsubscribe()`, `Observer.onNext()` will stop being called.
+
 ### Version 3.0
 * Added support for asynchronous callbacks via `IncrementalCallback` and `IncrementalDecoder.TextStream`.
 * Wire is now Logger, with configurable Logger.Level.
diff --git a/README.md b/README.md
index 29ff2e2aa6..c33

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