beginning work of adding limitation and validation framework to http parameters, playing with old oauth code
This commit is contained in:
parent
412b6eaeb5
commit
3c00b77d98
41 changed files with 2385 additions and 46 deletions
52
build.gradle
52
build.gradle
|
@ -2,6 +2,7 @@
|
||||||
plugins {
|
plugins {
|
||||||
id "org.sonarqube" version "2.6.1"
|
id "org.sonarqube" version "2.6.1"
|
||||||
id "io.codearte.nexus-staging" version "0.11.0"
|
id "io.codearte.nexus-staging" version "0.11.0"
|
||||||
|
id "com.github.spotbugs" version "1.6.9"
|
||||||
id "org.xbib.gradle.plugin.asciidoctor" version "1.5.6.0.1"
|
id "org.xbib.gradle.plugin.asciidoctor" version "1.5.6.0.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,7 +26,7 @@ subprojects {
|
||||||
apply plugin: 'maven'
|
apply plugin: 'maven'
|
||||||
apply plugin: 'signing'
|
apply plugin: 'signing'
|
||||||
apply plugin: 'checkstyle'
|
apply plugin: 'checkstyle'
|
||||||
apply plugin: 'findbugs'
|
apply plugin: 'com.github.spotbugs'
|
||||||
apply plugin: 'pmd'
|
apply plugin: 'pmd'
|
||||||
apply plugin: 'org.xbib.gradle.plugin.asciidoctor'
|
apply plugin: 'org.xbib.gradle.plugin.asciidoctor'
|
||||||
|
|
||||||
|
@ -91,7 +92,7 @@ subprojects {
|
||||||
javadoc {
|
javadoc {
|
||||||
options.docletpath = configurations.asciidoclet.files.asType(List)
|
options.docletpath = configurations.asciidoclet.files.asType(List)
|
||||||
options.doclet = 'org.asciidoctor.Asciidoclet'
|
options.doclet = 'org.asciidoctor.Asciidoclet'
|
||||||
options.overview = "src/docs/asciidoclet/overview.adoc"
|
//options.overview = "src/docs/asciidoclet/overview.adoc"
|
||||||
options.addStringOption "-base-dir", "${projectDir}"
|
options.addStringOption "-base-dir", "${projectDir}"
|
||||||
options.addStringOption "-attribute",
|
options.addStringOption "-attribute",
|
||||||
"name=${project.name},version=${project.version},title-link=https://github.com/xbib/${project.name}"
|
"name=${project.name},version=${project.version},title-link=https://github.com/xbib/${project.name}"
|
||||||
|
@ -120,7 +121,52 @@ subprojects {
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "${rootProject.projectDir}/gradle/publish.gradle"
|
apply from: "${rootProject.projectDir}/gradle/publish.gradle"
|
||||||
apply from: "${rootProject.projectDir}/gradle/qa.gradle"
|
|
||||||
|
tasks.withType(Checkstyle) {
|
||||||
|
ignoreFailures = true
|
||||||
|
reports {
|
||||||
|
xml.enabled = true
|
||||||
|
html.enabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType(Pmd) {
|
||||||
|
ignoreFailures = true
|
||||||
|
reports {
|
||||||
|
xml.enabled = true
|
||||||
|
html.enabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkstyle {
|
||||||
|
configFile = rootProject.file('config/checkstyle/checkstyle.xml')
|
||||||
|
ignoreFailures = true
|
||||||
|
showViolations = true
|
||||||
|
}
|
||||||
|
|
||||||
|
sonarqube {
|
||||||
|
properties {
|
||||||
|
property "sonar.projectName", "${project.group} ${project.name}"
|
||||||
|
property "sonar.sourceEncoding", "UTF-8"
|
||||||
|
property "sonar.tests", "src/test/java"
|
||||||
|
property "sonar.scm.provider", "git"
|
||||||
|
property "sonar.junit.reportsPath", "build/test-results/test/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spotbugs {
|
||||||
|
effort = "max"
|
||||||
|
reportLevel = "low"
|
||||||
|
//includeFilter = file("findbugs-exclude.xml")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType(com.github.spotbugs.SpotBugsTask) {
|
||||||
|
ignoreFailures = true
|
||||||
|
reports {
|
||||||
|
xml.enabled = false
|
||||||
|
html.enabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nexusStaging {
|
nexusStaging {
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
group = org.xbib
|
group = org.xbib
|
||||||
name = net
|
name = net
|
||||||
version = 1.1.3
|
version = 1.2.0
|
||||||
|
|
||||||
jackson.version = 2.8.11
|
jackson.version = 2.8.11
|
||||||
junit.version = 4.12
|
junit.version = 4.12
|
||||||
wagon.version = 3.0.0
|
wagon.version = 3.0.0
|
||||||
asciidoclet.version = 1.5.4
|
asciidoclet.version = 1.5.4
|
||||||
|
|
||||||
|
org.gradle.warning.mode=all
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
tasks.withType(Checkstyle) {
|
|
||||||
ignoreFailures = true
|
|
||||||
reports {
|
|
||||||
xml.enabled = true
|
|
||||||
html.enabled = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tasks.withType(FindBugs) {
|
|
||||||
ignoreFailures = true
|
|
||||||
reports {
|
|
||||||
xml.enabled = false
|
|
||||||
html.enabled = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tasks.withType(Pmd) {
|
|
||||||
ignoreFailures = true
|
|
||||||
reports {
|
|
||||||
xml.enabled = true
|
|
||||||
html.enabled = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkstyle {
|
|
||||||
configFile = rootProject.file('config/checkstyle/checkstyle.xml')
|
|
||||||
ignoreFailures = true
|
|
||||||
showViolations = true
|
|
||||||
}
|
|
||||||
|
|
||||||
sonarqube {
|
|
||||||
properties {
|
|
||||||
property "sonar.projectName", "${project.group} ${project.name}"
|
|
||||||
property "sonar.sourceEncoding", "UTF-8"
|
|
||||||
property "sonar.tests", "src/test/java"
|
|
||||||
property "sonar.scm.provider", "git"
|
|
||||||
property "sonar.junit.reportsPath", "build/test-results/test/"
|
|
||||||
}
|
|
||||||
}
|
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,6 +1,6 @@
|
||||||
#Fri Sep 14 16:19:33 CEST 2018
|
#Thu Jan 17 15:23:55 CET 2019
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1-all.zip
|
||||||
|
|
2
gradlew
vendored
2
gradlew
vendored
|
@ -28,7 +28,7 @@ APP_NAME="Gradle"
|
||||||
APP_BASE_NAME=`basename "$0"`
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
DEFAULT_JVM_OPTS=""
|
DEFAULT_JVM_OPTS='"-Xmx64m"'
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD="maximum"
|
MAX_FD="maximum"
|
||||||
|
|
2
gradlew.bat
vendored
2
gradlew.bat
vendored
|
@ -14,7 +14,7 @@ set APP_BASE_NAME=%~n0
|
||||||
set APP_HOME=%DIRNAME%
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
@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 DEFAULT_JVM_OPTS="-Xmx64m"
|
||||||
|
|
||||||
@rem Find java.exe
|
@rem Find java.exe
|
||||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
3
net-http/build.gradle
Normal file
3
net-http/build.gradle
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
dependencies {
|
||||||
|
compile project(':net-url')
|
||||||
|
}
|
298
net-http/src/main/java/org/xbib/net/http/HttpParameters.java
Normal file
298
net-http/src/main/java/org/xbib/net/http/HttpParameters.java
Normal file
|
@ -0,0 +1,298 @@
|
||||||
|
package org.xbib.net.http;
|
||||||
|
|
||||||
|
import org.xbib.net.PercentDecoder;
|
||||||
|
import org.xbib.net.PercentEncoder;
|
||||||
|
import org.xbib.net.PercentEncoders;
|
||||||
|
import org.xbib.net.http.util.LimitedSortedStringSet;
|
||||||
|
import org.xbib.net.http.util.LimitedStringMap;
|
||||||
|
|
||||||
|
import java.nio.charset.MalformedInputException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.charset.UnmappableCharacterException;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.SortedSet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A limited multi-map of HTTP request parameters. Each key references a
|
||||||
|
* limited set of parameters collected from the request during message
|
||||||
|
* signing. Parameter values are sorted as per
|
||||||
|
* <a href="http://oauth.net/core/1.0a/#anchor13">OAuth specification</a></a>.
|
||||||
|
* Every key/value pair will be percent-encoded upon insertion.
|
||||||
|
* This class has special semantics tailored to
|
||||||
|
* being useful for message signing; it's not a general purpose collection class
|
||||||
|
* to handle request parameters.
|
||||||
|
*/
|
||||||
|
public class HttpParameters implements Map<String, SortedSet<String>> {
|
||||||
|
|
||||||
|
private final LimitedStringMap wrappedMap;
|
||||||
|
|
||||||
|
private final PercentEncoder percentEncoder;
|
||||||
|
|
||||||
|
private final PercentDecoder percentDecoder;
|
||||||
|
|
||||||
|
public HttpParameters() {
|
||||||
|
this.wrappedMap = new LimitedStringMap();
|
||||||
|
this.percentEncoder = PercentEncoders.getQueryEncoder(StandardCharsets.UTF_8);
|
||||||
|
this.percentDecoder = new PercentDecoder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SortedSet<String> put(String key, SortedSet<String> value) {
|
||||||
|
return wrappedMap.put(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SortedSet<String> get(Object key) {
|
||||||
|
return wrappedMap.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void putAll(Map<? extends String, ? extends SortedSet<String>> m) {
|
||||||
|
wrappedMap.putAll(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean containsKey(Object key) {
|
||||||
|
return wrappedMap.containsKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean containsValue(Object value) {
|
||||||
|
if (value instanceof String) {
|
||||||
|
for (Set<String> values : wrappedMap.values()) {
|
||||||
|
if (values.contains(value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int size() {
|
||||||
|
int count = 0;
|
||||||
|
for (String key : wrappedMap.keySet()) {
|
||||||
|
count += wrappedMap.get(key).size();
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return wrappedMap.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clear() {
|
||||||
|
wrappedMap.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SortedSet<String> remove(Object key) {
|
||||||
|
return wrappedMap.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<String> keySet() {
|
||||||
|
return wrappedMap.keySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<SortedSet<String>> values() {
|
||||||
|
return wrappedMap.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<Entry<String, SortedSet<String>>> entrySet() {
|
||||||
|
return wrappedMap.entrySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SortedSet<String> put(String key, SortedSet<String> values, boolean percentEncode)
|
||||||
|
throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
if (percentEncode) {
|
||||||
|
remove(key);
|
||||||
|
for (String v : values) {
|
||||||
|
put(key, v, true);
|
||||||
|
}
|
||||||
|
return get(key);
|
||||||
|
} else {
|
||||||
|
return wrappedMap.put(key, values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method to add a single value for the parameter specified by 'key'.
|
||||||
|
*
|
||||||
|
* @param key the parameter name
|
||||||
|
* @param value the parameter value
|
||||||
|
* @return the value
|
||||||
|
*/
|
||||||
|
public String put(String key, String value)
|
||||||
|
throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
return put(key, value, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method to add a single value for the parameter specified by
|
||||||
|
* 'key'.
|
||||||
|
*
|
||||||
|
* @param key the parameter name
|
||||||
|
* @param value the parameter value
|
||||||
|
* @param percentEncode whether key and value should be percent encoded before being
|
||||||
|
* inserted into the map
|
||||||
|
* @return the value
|
||||||
|
*/
|
||||||
|
public String put(String key, String value, boolean percentEncode)
|
||||||
|
throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
String k = percentEncode ? percentEncoder.encode(key) : key;
|
||||||
|
SortedSet<String> values = wrappedMap.get(k);
|
||||||
|
if (values == null) {
|
||||||
|
values = new LimitedSortedStringSet();
|
||||||
|
wrappedMap.put(k, values);
|
||||||
|
}
|
||||||
|
String v = null;
|
||||||
|
if (value != null) {
|
||||||
|
v = percentEncode ? percentEncoder.encode(value) : value;
|
||||||
|
values.add(v);
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method to allow for storing null values. {@link #put} doesn't
|
||||||
|
* allow null values, because that would be ambiguous.
|
||||||
|
*
|
||||||
|
* @param key the parameter name
|
||||||
|
* @param nullString can be anything, but probably... null?
|
||||||
|
* @return null
|
||||||
|
*/
|
||||||
|
public String putNull(String key, String nullString)
|
||||||
|
throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
return put(key, nullString);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void putAll(Map<? extends String, ? extends SortedSet<String>> m, boolean percentEncode)
|
||||||
|
throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
if (percentEncode) {
|
||||||
|
for (String key : m.keySet()) {
|
||||||
|
put(key, m.get(key), true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
wrappedMap.putAll(m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void putAll(String[] keyValuePairs, boolean percentEncode)
|
||||||
|
throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
for (int i = 0; i < keyValuePairs.length - 1; i += 2) {
|
||||||
|
this.put(keyValuePairs[i], keyValuePairs[i + 1], percentEncode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method to merge a Map<String, List<String>>.
|
||||||
|
*
|
||||||
|
* @param m the map
|
||||||
|
*/
|
||||||
|
public void putMap(Map<String, List<String>> m) {
|
||||||
|
for (String key : m.keySet()) {
|
||||||
|
SortedSet<String> vals = get(key);
|
||||||
|
if (vals == null) {
|
||||||
|
vals = new LimitedSortedStringSet();
|
||||||
|
put(key, vals);
|
||||||
|
}
|
||||||
|
vals.addAll(m.get(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public String getFirst(String key)
|
||||||
|
throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
return getFirst(key, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the first value from the set of all values for the given
|
||||||
|
* parameter name. If the key passed to this method contains special
|
||||||
|
* characters, you must first percent encode it, otherwise the lookup will fail
|
||||||
|
* (that's because upon storing values in this map, keys get
|
||||||
|
* percent-encoded).
|
||||||
|
*
|
||||||
|
* @param key the parameter name (must be percent encoded if it contains unsafe
|
||||||
|
* characters!)
|
||||||
|
* @param percentDecode whether the value being retrieved should be percent decoded
|
||||||
|
* @return the first value found for this parameter
|
||||||
|
*/
|
||||||
|
public String getFirst(String key, boolean percentDecode)
|
||||||
|
throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
SortedSet<String> values = wrappedMap.get(key);
|
||||||
|
if (values == null || values.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String value = values.first();
|
||||||
|
return percentDecode ? percentDecoder.decode(value) : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Concatenates all values for the given key to a list of key/value pairs
|
||||||
|
* suitable for use in a URL query string.
|
||||||
|
*
|
||||||
|
* @param key the parameter name
|
||||||
|
* @return the query string
|
||||||
|
*/
|
||||||
|
public String getAsQueryString(String key)
|
||||||
|
throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
return getAsQueryString(key, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Concatenates all values for the given key to a list of key/value pairs
|
||||||
|
* suitable for use in a URL query string.
|
||||||
|
*
|
||||||
|
* @param key the parameter name
|
||||||
|
* @param percentEncode whether key should be percent encoded before being
|
||||||
|
* used with the map
|
||||||
|
* @return the query string
|
||||||
|
*/
|
||||||
|
public String getAsQueryString(String key, boolean percentEncode)
|
||||||
|
throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
String k = percentEncode ? percentEncoder.encode(key) : key;
|
||||||
|
SortedSet<String> values = wrappedMap.get(k);
|
||||||
|
if (values == null) {
|
||||||
|
return k + "=";
|
||||||
|
}
|
||||||
|
Iterator<String> it = values.iterator();
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
sb.append(k).append("=").append(it.next());
|
||||||
|
if (it.hasNext()) {
|
||||||
|
sb.append("&");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAsHeaderElement(String key)
|
||||||
|
throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
String value = getFirst(key);
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return key + "=\"" + value + "\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpParameters getOAuthParameters() {
|
||||||
|
HttpParameters oauthParams = new HttpParameters();
|
||||||
|
for (Entry<String, SortedSet<String>> param : this.entrySet()) {
|
||||||
|
String key = param.getKey();
|
||||||
|
if (key.startsWith("oauth_") || key.startsWith("x_oauth_")) {
|
||||||
|
oauthParams.put(key, param.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return oauthParams;
|
||||||
|
}
|
||||||
|
}
|
30
net-http/src/main/java/org/xbib/net/http/HttpRequest.java
Normal file
30
net-http/src/main/java/org/xbib/net/http/HttpRequest.java
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
package org.xbib.net.http;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A representation of an HTTP request. Contains methods to access all
|
||||||
|
* those parts of an HTTP request.
|
||||||
|
*/
|
||||||
|
public interface HttpRequest {
|
||||||
|
|
||||||
|
String getMethod();
|
||||||
|
|
||||||
|
String getRequestUrl();
|
||||||
|
|
||||||
|
void setRequestUrl(String url);
|
||||||
|
|
||||||
|
void setHeader(String name, String value);
|
||||||
|
|
||||||
|
String getHeader(String name);
|
||||||
|
|
||||||
|
Map<String, String> getAllHeaders();
|
||||||
|
|
||||||
|
InputStream getMessagePayload() throws IOException;
|
||||||
|
|
||||||
|
String getContentType();
|
||||||
|
|
||||||
|
Object unwrap();
|
||||||
|
}
|
15
net-http/src/main/java/org/xbib/net/http/HttpResponse.java
Normal file
15
net-http/src/main/java/org/xbib/net/http/HttpResponse.java
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package org.xbib.net.http;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
public interface HttpResponse {
|
||||||
|
|
||||||
|
int getStatusCode() throws IOException;
|
||||||
|
|
||||||
|
String getReasonPhrase() throws Exception;
|
||||||
|
|
||||||
|
InputStream getContent() throws IOException;
|
||||||
|
|
||||||
|
Object unwrap();
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
package org.xbib.net.http;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class UrlStringRequestAdapter implements HttpRequest {
|
||||||
|
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
public UrlStringRequestAdapter(String url) {
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMethod() {
|
||||||
|
return "GET";
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRequestUrl() {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRequestUrl(String url) {
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHeader(String name, String value) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getHeader(String name) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getAllHeaders() {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputStream getMessagePayload() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContentType() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object unwrap() {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package org.xbib.net.http.util;
|
||||||
|
|
||||||
|
import java.util.SortedSet;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
|
public class LimitedSortedStringSet extends TreeSet<String> implements SortedSet<String> {
|
||||||
|
|
||||||
|
private final int sizeLimit;
|
||||||
|
|
||||||
|
private final int elementSizeLimit;
|
||||||
|
|
||||||
|
public LimitedSortedStringSet() {
|
||||||
|
this(1024, 65536);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LimitedSortedStringSet(int sizeLimit, int elementSizeLimit) {
|
||||||
|
this.sizeLimit = sizeLimit;
|
||||||
|
this.elementSizeLimit = elementSizeLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean add(String string) {
|
||||||
|
if (size() < sizeLimit && string.length() <= elementSizeLimit ) {
|
||||||
|
return super.add(string);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package org.xbib.net.http.util;
|
||||||
|
|
||||||
|
import java.util.SortedSet;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
public class LimitedStringMap extends TreeMap<String, SortedSet<String>> {
|
||||||
|
|
||||||
|
private final int limit;
|
||||||
|
|
||||||
|
public LimitedStringMap() {
|
||||||
|
this(1024);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LimitedStringMap(int limit) {
|
||||||
|
this.limit = limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SortedSet<String> put(String key, SortedSet<String> value) {
|
||||||
|
if (size() < limit) {
|
||||||
|
return super.put(key, value);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
3
net-oauth/build.gradle
Normal file
3
net-oauth/build.gradle
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
dependencies {
|
||||||
|
compile project(':net-http')
|
||||||
|
}
|
|
@ -0,0 +1,237 @@
|
||||||
|
package org.xbib.net.oauth;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.MalformedInputException;
|
||||||
|
import java.nio.charset.UnmappableCharacterException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
import org.xbib.net.http.HttpParameters;
|
||||||
|
import org.xbib.net.http.HttpRequest;
|
||||||
|
import org.xbib.net.http.UrlStringRequestAdapter;
|
||||||
|
import org.xbib.net.oauth.sign.AuthorizationHeaderSigningStrategy;
|
||||||
|
import org.xbib.net.oauth.sign.HmacSha1MessageSigner;
|
||||||
|
import org.xbib.net.oauth.sign.OAuthMessageSigner;
|
||||||
|
import org.xbib.net.oauth.sign.QueryStringSigningStrategy;
|
||||||
|
import org.xbib.net.oauth.sign.SigningStrategy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ABC for consumer implementations. If you're developing a custom consumer you
|
||||||
|
* will probably inherit from this class to save you a lot of work.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public abstract class AbstractOAuthConsumer implements OAuthConsumer {
|
||||||
|
|
||||||
|
private String consumerKey, consumerSecret;
|
||||||
|
|
||||||
|
private String token;
|
||||||
|
|
||||||
|
private OAuthMessageSigner messageSigner;
|
||||||
|
|
||||||
|
private SigningStrategy signingStrategy;
|
||||||
|
|
||||||
|
// these are params that may be passed to the consumer directly (i.e.
|
||||||
|
// without going through the request object)
|
||||||
|
private HttpParameters additionalParameters;
|
||||||
|
|
||||||
|
// these are the params which will be passed to the message signer
|
||||||
|
private HttpParameters requestParameters;
|
||||||
|
|
||||||
|
private boolean sendEmptyTokens;
|
||||||
|
|
||||||
|
private final Random random = new SecureRandom();
|
||||||
|
|
||||||
|
public AbstractOAuthConsumer(String consumerKey, String consumerSecret) {
|
||||||
|
this.consumerKey = consumerKey;
|
||||||
|
this.consumerSecret = consumerSecret;
|
||||||
|
setMessageSigner(new HmacSha1MessageSigner());
|
||||||
|
setSigningStrategy(new AuthorizationHeaderSigningStrategy());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMessageSigner(OAuthMessageSigner messageSigner) {
|
||||||
|
this.messageSigner = messageSigner;
|
||||||
|
messageSigner.setConsumerSecret(consumerSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSigningStrategy(SigningStrategy signingStrategy) {
|
||||||
|
this.signingStrategy = signingStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAdditionalParameters(HttpParameters additionalParameters) {
|
||||||
|
this.additionalParameters = additionalParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized HttpRequest sign(HttpRequest request) throws OAuthMessageSignerException,
|
||||||
|
OAuthExpectationFailedException, OAuthCommunicationException {
|
||||||
|
if (consumerKey == null) {
|
||||||
|
throw new OAuthExpectationFailedException("consumer key not set");
|
||||||
|
}
|
||||||
|
if (consumerSecret == null) {
|
||||||
|
throw new OAuthExpectationFailedException("consumer secret not set");
|
||||||
|
}
|
||||||
|
requestParameters = new HttpParameters();
|
||||||
|
try {
|
||||||
|
if (additionalParameters != null) {
|
||||||
|
requestParameters.putAll(additionalParameters, false);
|
||||||
|
}
|
||||||
|
collectHeaderParameters(request, requestParameters);
|
||||||
|
collectQueryParameters(request, requestParameters);
|
||||||
|
collectBodyParameters(request, requestParameters);
|
||||||
|
// add any OAuth params that haven't already been set
|
||||||
|
completeOAuthParameters(requestParameters);
|
||||||
|
requestParameters.remove(OAuth.OAUTH_SIGNATURE);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new OAuthCommunicationException(e);
|
||||||
|
}
|
||||||
|
String signature = messageSigner.sign(request, requestParameters);
|
||||||
|
try {
|
||||||
|
signingStrategy.writeSignature(signature, request, requestParameters);
|
||||||
|
} catch (MalformedInputException | UnmappableCharacterException e) {
|
||||||
|
throw new OAuthMessageSignerException(e);
|
||||||
|
}
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized HttpRequest sign(Object request) throws OAuthMessageSignerException,
|
||||||
|
OAuthExpectationFailedException, OAuthCommunicationException {
|
||||||
|
return sign(wrap(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized String sign(String url) throws OAuthMessageSignerException,
|
||||||
|
OAuthExpectationFailedException, OAuthCommunicationException {
|
||||||
|
HttpRequest request = new UrlStringRequestAdapter(url);
|
||||||
|
// switch to URL signing
|
||||||
|
SigningStrategy oldStrategy = this.signingStrategy;
|
||||||
|
this.signingStrategy = new QueryStringSigningStrategy();
|
||||||
|
sign(request);
|
||||||
|
// revert to old strategy
|
||||||
|
this.signingStrategy = oldStrategy;
|
||||||
|
return request.getRequestUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapts the given request object to a Signpost {@link HttpRequest}. How
|
||||||
|
* this is done depends on the consumer implementation.
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* the native HTTP request instance
|
||||||
|
* @return the adapted request
|
||||||
|
*/
|
||||||
|
protected abstract HttpRequest wrap(Object request);
|
||||||
|
|
||||||
|
public void setTokenWithSecret(String token, String tokenSecret) {
|
||||||
|
this.token = token;
|
||||||
|
messageSigner.setTokenSecret(tokenSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getToken() {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTokenSecret() {
|
||||||
|
return messageSigner.getTokenSecret();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getConsumerKey() {
|
||||||
|
return this.consumerKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getConsumerSecret() {
|
||||||
|
return this.consumerSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* Helper method that adds any OAuth parameters to the given request
|
||||||
|
* parameters which are missing from the current request but required for
|
||||||
|
* signing. A good example is the oauth_nonce parameter, which is typically
|
||||||
|
* not provided by the client in advance.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* It's probably not a very good idea to override this method. If you want
|
||||||
|
* to generate different nonces or timestamps, override
|
||||||
|
* {@link #generateNonce()} or {@link #generateTimestamp()} instead.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param out
|
||||||
|
* the request parameter which should be completed
|
||||||
|
*/
|
||||||
|
protected void completeOAuthParameters(HttpParameters out)
|
||||||
|
throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
if (!out.containsKey(OAuth.OAUTH_CONSUMER_KEY)) {
|
||||||
|
out.put(OAuth.OAUTH_CONSUMER_KEY, consumerKey, true);
|
||||||
|
}
|
||||||
|
if (!out.containsKey(OAuth.OAUTH_SIGNATURE_METHOD)) {
|
||||||
|
out.put(OAuth.OAUTH_SIGNATURE_METHOD, messageSigner.getSignatureMethod(), true);
|
||||||
|
}
|
||||||
|
if (!out.containsKey(OAuth.OAUTH_TIMESTAMP)) {
|
||||||
|
out.put(OAuth.OAUTH_TIMESTAMP, generateTimestamp(), true);
|
||||||
|
}
|
||||||
|
if (!out.containsKey(OAuth.OAUTH_NONCE)) {
|
||||||
|
out.put(OAuth.OAUTH_NONCE, generateNonce(), true);
|
||||||
|
}
|
||||||
|
if (!out.containsKey(OAuth.OAUTH_VERSION)) {
|
||||||
|
out.put(OAuth.OAUTH_VERSION, OAuth.VERSION_1_0, true);
|
||||||
|
}
|
||||||
|
if (!out.containsKey(OAuth.OAUTH_TOKEN)) {
|
||||||
|
if (token != null && !token.equals("") || sendEmptyTokens) {
|
||||||
|
out.put(OAuth.OAUTH_TOKEN, token, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpParameters getRequestParameters() {
|
||||||
|
return requestParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSendEmptyTokens(boolean enable) {
|
||||||
|
this.sendEmptyTokens = enable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collects OAuth Authorization header parameters as per OAuth Core 1.0 spec
|
||||||
|
* section 9.1.1
|
||||||
|
*/
|
||||||
|
protected void collectHeaderParameters(HttpRequest request, HttpParameters out)
|
||||||
|
throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
HttpParameters headerParams = OAuth.oauthHeaderToParamsMap(request.getHeader(OAuth.HTTP_AUTHORIZATION_HEADER));
|
||||||
|
out.putAll(headerParams, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collects x-www-form-urlencoded body parameters as per OAuth Core 1.0 spec
|
||||||
|
* section 9.1.1
|
||||||
|
*/
|
||||||
|
protected void collectBodyParameters(HttpRequest request, HttpParameters out)
|
||||||
|
throws IOException {
|
||||||
|
// collect x-www-form-urlencoded body params
|
||||||
|
String contentType = request.getContentType();
|
||||||
|
if (contentType != null && contentType.startsWith(OAuth.FORM_ENCODED)) {
|
||||||
|
InputStream payload = request.getMessagePayload();
|
||||||
|
out.putAll(OAuth.decodeForm(payload), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collects HTTP GET query string parameters as per OAuth Core 1.0 spec
|
||||||
|
* section 9.1.1
|
||||||
|
*/
|
||||||
|
protected void collectQueryParameters(HttpRequest request, HttpParameters out)
|
||||||
|
throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
String url = request.getRequestUrl();
|
||||||
|
int q = url.indexOf('?');
|
||||||
|
if (q >= 0) {
|
||||||
|
// Combine the URL query string with the other parameters:
|
||||||
|
out.putAll(OAuth.decodeForm(url.substring(q + 1)), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String generateTimestamp() {
|
||||||
|
return Long.toString(System.currentTimeMillis() / 1000L);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String generateNonce() {
|
||||||
|
return Long.toString(random.nextLong());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,299 @@
|
||||||
|
package org.xbib.net.oauth;
|
||||||
|
|
||||||
|
import org.xbib.net.http.HttpParameters;
|
||||||
|
import org.xbib.net.http.HttpRequest;
|
||||||
|
import org.xbib.net.http.HttpResponse;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.MalformedInputException;
|
||||||
|
import java.nio.charset.UnmappableCharacterException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For all provider implementations. If you're writing a custom provider,
|
||||||
|
* you will probably inherit from this class, since it takes a lot of work from
|
||||||
|
* you.
|
||||||
|
*/
|
||||||
|
public abstract class AbstractOAuthProvider implements OAuthProvider {
|
||||||
|
|
||||||
|
private final String requestTokenEndpointUrl;
|
||||||
|
|
||||||
|
private final String accessTokenEndpointUrl;
|
||||||
|
|
||||||
|
private final String authorizationWebsiteUrl;
|
||||||
|
|
||||||
|
private HttpParameters responseParameters;
|
||||||
|
|
||||||
|
private Map<String, String> defaultHeaders;
|
||||||
|
|
||||||
|
private boolean isOAuth10a;
|
||||||
|
|
||||||
|
private OAuthProviderListener listener;
|
||||||
|
|
||||||
|
public AbstractOAuthProvider(String requestTokenEndpointUrl, String accessTokenEndpointUrl,
|
||||||
|
String authorizationWebsiteUrl) {
|
||||||
|
this.requestTokenEndpointUrl = requestTokenEndpointUrl;
|
||||||
|
this.accessTokenEndpointUrl = accessTokenEndpointUrl;
|
||||||
|
this.authorizationWebsiteUrl = authorizationWebsiteUrl;
|
||||||
|
this.responseParameters = new HttpParameters();
|
||||||
|
this.defaultHeaders = new HashMap<String, String>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized String retrieveRequestToken(OAuthConsumer consumer, String callbackUrl,
|
||||||
|
String... customOAuthParams) throws OAuthMessageSignerException,
|
||||||
|
OAuthNotAuthorizedException, OAuthExpectationFailedException,
|
||||||
|
OAuthCommunicationException {
|
||||||
|
// invalidate current credentials, if any
|
||||||
|
consumer.setTokenWithSecret(null, null);
|
||||||
|
// 1.0a expects the callback to be sent while getting the request token.
|
||||||
|
// 1.0 service providers would simply ignore this parameter.
|
||||||
|
HttpParameters params = new HttpParameters();
|
||||||
|
try {
|
||||||
|
params.putAll(customOAuthParams, true);
|
||||||
|
params.put(OAuth.OAUTH_CALLBACK, callbackUrl, true);
|
||||||
|
retrieveToken(consumer, requestTokenEndpointUrl, params);
|
||||||
|
String callbackConfirmed = responseParameters.getFirst(OAuth.OAUTH_CALLBACK_CONFIRMED);
|
||||||
|
responseParameters.remove(OAuth.OAUTH_CALLBACK_CONFIRMED);
|
||||||
|
isOAuth10a = Boolean.TRUE.toString().equals(callbackConfirmed);
|
||||||
|
// 1.0 service providers expect the callback as part of the auth URL,
|
||||||
|
// Do not send when 1.0a.
|
||||||
|
if (isOAuth10a) {
|
||||||
|
return OAuth.addQueryParameters(authorizationWebsiteUrl, OAuth.OAUTH_TOKEN,
|
||||||
|
consumer.getToken());
|
||||||
|
} else {
|
||||||
|
return OAuth.addQueryParameters(authorizationWebsiteUrl, OAuth.OAUTH_TOKEN,
|
||||||
|
consumer.getToken(), OAuth.OAUTH_CALLBACK, callbackUrl);
|
||||||
|
}
|
||||||
|
} catch (MalformedInputException | UnmappableCharacterException e) {
|
||||||
|
throw new OAuthMessageSignerException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void retrieveAccessToken(OAuthConsumer consumer, String oauthVerifier,
|
||||||
|
String... customOAuthParams) throws OAuthMessageSignerException,
|
||||||
|
OAuthNotAuthorizedException, OAuthExpectationFailedException,
|
||||||
|
OAuthCommunicationException {
|
||||||
|
if (consumer.getToken() == null || consumer.getTokenSecret() == null) {
|
||||||
|
throw new OAuthExpectationFailedException(
|
||||||
|
"Authorized request token or token secret not set. "
|
||||||
|
+ "Did you retrieve an authorized request token before?");
|
||||||
|
}
|
||||||
|
HttpParameters params = new HttpParameters();
|
||||||
|
try {
|
||||||
|
params.putAll(customOAuthParams, true);
|
||||||
|
if (isOAuth10a && oauthVerifier != null) {
|
||||||
|
params.put(OAuth.OAUTH_VERIFIER, oauthVerifier, true);
|
||||||
|
}
|
||||||
|
} catch (MalformedInputException | UnmappableCharacterException e) {
|
||||||
|
throw new OAuthMessageSignerException(e);
|
||||||
|
}
|
||||||
|
retrieveToken(consumer, accessTokenEndpointUrl, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implemented by subclasses. The responsibility of this method is to
|
||||||
|
* contact the service provider at the given endpoint URL and fetch a
|
||||||
|
* request or access token. What kind of token is retrieved solely depends
|
||||||
|
* on the URL being used.
|
||||||
|
* Correct implementations of this method must guarantee the following
|
||||||
|
* post-conditions:
|
||||||
|
* <ul>
|
||||||
|
* <li>the {@link OAuthConsumer} passed to this method must have a valid
|
||||||
|
* {@link OAuth#OAUTH_TOKEN} and {@link OAuth#OAUTH_TOKEN_SECRET} set by
|
||||||
|
* calling {@link OAuthConsumer#setTokenWithSecret(String, String)}</li>
|
||||||
|
* <li>{@link #getResponseParameters()} must return the set of query
|
||||||
|
* parameters served by the service provider in the token response, with all
|
||||||
|
* OAuth specific parameters being removed</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @param consumer the {@link OAuthConsumer} that should be used to sign the request
|
||||||
|
* @param endpointUrl the URL at which the service provider serves the OAuth token that
|
||||||
|
* is to be fetched
|
||||||
|
* @param customOAuthParams you can pass custom OAuth parameters here (such as oauth_callback
|
||||||
|
* or oauth_verifier) which will go directly into the signer, i.e.
|
||||||
|
* you don't have to put them into the request first.
|
||||||
|
* @throws OAuthMessageSignerException if signing the token request fails
|
||||||
|
* @throws OAuthCommunicationException if a network communication error occurs
|
||||||
|
* @throws OAuthNotAuthorizedException if the server replies 401 - Unauthorized
|
||||||
|
* @throws OAuthExpectationFailedException if an expectation has failed, e.g. because the server didn't
|
||||||
|
* reply in the expected format
|
||||||
|
*/
|
||||||
|
protected void retrieveToken(OAuthConsumer consumer, String endpointUrl,
|
||||||
|
HttpParameters customOAuthParams) throws OAuthMessageSignerException,
|
||||||
|
OAuthCommunicationException, OAuthNotAuthorizedException,
|
||||||
|
OAuthExpectationFailedException {
|
||||||
|
Map<String, String> defaultHeaders = getRequestHeaders();
|
||||||
|
if (consumer.getConsumerKey() == null || consumer.getConsumerSecret() == null) {
|
||||||
|
throw new OAuthExpectationFailedException("Consumer key or secret not set");
|
||||||
|
}
|
||||||
|
HttpRequest request = null;
|
||||||
|
HttpResponse response = null;
|
||||||
|
try {
|
||||||
|
request = createRequest(endpointUrl);
|
||||||
|
for (String header : defaultHeaders.keySet()) {
|
||||||
|
request.setHeader(header, defaultHeaders.get(header));
|
||||||
|
}
|
||||||
|
if (customOAuthParams != null && !customOAuthParams.isEmpty()) {
|
||||||
|
consumer.setAdditionalParameters(customOAuthParams);
|
||||||
|
}
|
||||||
|
if (this.listener != null) {
|
||||||
|
this.listener.prepareRequest(request);
|
||||||
|
}
|
||||||
|
consumer.sign(request);
|
||||||
|
if (this.listener != null) {
|
||||||
|
this.listener.prepareSubmission(request);
|
||||||
|
}
|
||||||
|
response = sendRequest(request);
|
||||||
|
int statusCode = response.getStatusCode();
|
||||||
|
boolean requestHandled = false;
|
||||||
|
if (this.listener != null) {
|
||||||
|
requestHandled = this.listener.onResponseReceived(request, response);
|
||||||
|
}
|
||||||
|
if (requestHandled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (statusCode >= 300) {
|
||||||
|
handleUnexpectedResponse(statusCode, response);
|
||||||
|
}
|
||||||
|
HttpParameters responseParams = OAuth.decodeForm(response.getContent());
|
||||||
|
String token = responseParams.getFirst(OAuth.OAUTH_TOKEN);
|
||||||
|
String secret = responseParams.getFirst(OAuth.OAUTH_TOKEN_SECRET);
|
||||||
|
responseParams.remove(OAuth.OAUTH_TOKEN);
|
||||||
|
responseParams.remove(OAuth.OAUTH_TOKEN_SECRET);
|
||||||
|
setResponseParameters(responseParams);
|
||||||
|
if (token == null || secret == null) {
|
||||||
|
throw new OAuthExpectationFailedException(
|
||||||
|
"Request token or token secret not set in server reply. "
|
||||||
|
+ "The service provider you use is probably buggy.");
|
||||||
|
}
|
||||||
|
consumer.setTokenWithSecret(token, secret);
|
||||||
|
} catch (OAuthNotAuthorizedException | OAuthExpectationFailedException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new OAuthCommunicationException(e);
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
closeConnection(request, response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new OAuthCommunicationException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void handleUnexpectedResponse(int statusCode, HttpResponse response) throws Exception {
|
||||||
|
if (response == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(response.getContent()));
|
||||||
|
StringBuilder responseBody = new StringBuilder();
|
||||||
|
String line = reader.readLine();
|
||||||
|
while (line != null) {
|
||||||
|
responseBody.append(line);
|
||||||
|
line = reader.readLine();
|
||||||
|
}
|
||||||
|
if (statusCode == 401) {
|
||||||
|
throw new OAuthNotAuthorizedException(responseBody.toString());
|
||||||
|
}
|
||||||
|
throw new OAuthCommunicationException("Service provider responded in error: "
|
||||||
|
+ statusCode + " (" + response.getReasonPhrase() + ")", responseBody.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrride this method if you want to customize the logic for building a
|
||||||
|
* request object for the given endpoint URL.
|
||||||
|
*
|
||||||
|
* @param endpointUrl
|
||||||
|
* the URL to which the request will go
|
||||||
|
* @return the request object
|
||||||
|
* @throws Exception
|
||||||
|
* if something breaks
|
||||||
|
*/
|
||||||
|
protected abstract HttpRequest createRequest(String endpointUrl) throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override this method if you want to customize the logic for how the given
|
||||||
|
* request is sent to the server.
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* the request to send
|
||||||
|
* @return the response to the request
|
||||||
|
* @throws Exception
|
||||||
|
* if something breaks
|
||||||
|
*/
|
||||||
|
protected abstract HttpResponse sendRequest(HttpRequest request) throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the connection is being finalized after receiving the
|
||||||
|
* response. Use this to do any cleanup / resource freeing.
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* the request that has been sent
|
||||||
|
* @param response
|
||||||
|
* the response that has been received
|
||||||
|
* @throws Exception
|
||||||
|
* if something breaks
|
||||||
|
*/
|
||||||
|
protected void closeConnection(HttpRequest request, HttpResponse response) throws Exception {
|
||||||
|
// NOP
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpParameters getResponseParameters() {
|
||||||
|
return responseParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a single query parameter as served by the service provider in a
|
||||||
|
* token reply. You must call {@link #setResponseParameters} with the set of
|
||||||
|
* parameters before using this method.
|
||||||
|
*
|
||||||
|
* @param key
|
||||||
|
* the parameter name
|
||||||
|
* @return the parameter value
|
||||||
|
*/
|
||||||
|
protected String getResponseParameter(String key)
|
||||||
|
throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
return responseParameters.getFirst(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setResponseParameters(HttpParameters parameters) {
|
||||||
|
this.responseParameters = parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOAuth10a(boolean isOAuth10aProvider) {
|
||||||
|
this.isOAuth10a = isOAuth10aProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOAuth10a() {
|
||||||
|
return isOAuth10a;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRequestTokenEndpointUrl() {
|
||||||
|
return this.requestTokenEndpointUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAccessTokenEndpointUrl() {
|
||||||
|
return this.accessTokenEndpointUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAuthorizationWebsiteUrl() {
|
||||||
|
return this.authorizationWebsiteUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRequestHeader(String header, String value) {
|
||||||
|
defaultHeaders.put(header, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getRequestHeaders() {
|
||||||
|
return defaultHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setListener(OAuthProviderListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeListener(OAuthProviderListener listener) {
|
||||||
|
this.listener = null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package org.xbib.net.oauth;
|
||||||
|
|
||||||
|
import org.xbib.net.http.HttpRequest;
|
||||||
|
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default implementation for an OAuth consumer. Only supports signing
|
||||||
|
* {@link HttpURLConnection} type requests.
|
||||||
|
*/
|
||||||
|
public class DefaultOAuthConsumer extends AbstractOAuthConsumer {
|
||||||
|
|
||||||
|
public DefaultOAuthConsumer(String consumerKey, String consumerSecret) {
|
||||||
|
super(consumerKey, consumerSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected HttpRequest wrap(Object request) {
|
||||||
|
if (!(request instanceof HttpURLConnection)) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"The default consumer expects requests of type java.net.HttpURLConnection");
|
||||||
|
}
|
||||||
|
return new HttpURLConnectionRequestAdapter((HttpURLConnection) request);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
package org.xbib.net.oauth;
|
||||||
|
|
||||||
|
import org.xbib.net.http.HttpRequest;
|
||||||
|
import org.xbib.net.http.HttpResponse;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This default implementation uses {@link HttpURLConnection} type GET
|
||||||
|
* requests to receive tokens from a service provider.
|
||||||
|
*/
|
||||||
|
public class DefaultOAuthProvider extends AbstractOAuthProvider {
|
||||||
|
|
||||||
|
public DefaultOAuthProvider(String requestTokenEndpointUrl, String accessTokenEndpointUrl,
|
||||||
|
String authorizationWebsiteUrl) {
|
||||||
|
super(requestTokenEndpointUrl, accessTokenEndpointUrl, authorizationWebsiteUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected HttpRequest createRequest(String endpointUrl) throws IOException {
|
||||||
|
HttpURLConnection connection = (HttpURLConnection) new URL(endpointUrl).openConnection();
|
||||||
|
connection.setRequestMethod("POST");
|
||||||
|
connection.setAllowUserInteraction(false);
|
||||||
|
connection.setRequestProperty("Content-Length", "0");
|
||||||
|
return new HttpURLConnectionRequestAdapter(connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected HttpResponse sendRequest(HttpRequest request) throws IOException {
|
||||||
|
HttpURLConnection connection = (HttpURLConnection) request.unwrap();
|
||||||
|
connection.connect();
|
||||||
|
return new HttpURLConnectionResponseAdapter(connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void closeConnection(HttpRequest request, HttpResponse response) {
|
||||||
|
HttpURLConnection connection = (HttpURLConnection) request.unwrap();
|
||||||
|
if (connection != null) {
|
||||||
|
connection.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package org.xbib.net.oauth;
|
||||||
|
|
||||||
|
import org.xbib.net.http.HttpRequest;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class HttpURLConnectionRequestAdapter implements HttpRequest {
|
||||||
|
|
||||||
|
protected HttpURLConnection connection;
|
||||||
|
|
||||||
|
public HttpURLConnectionRequestAdapter(HttpURLConnection connection) {
|
||||||
|
this.connection = connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMethod() {
|
||||||
|
return connection.getRequestMethod();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRequestUrl() {
|
||||||
|
return connection.getURL().toExternalForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRequestUrl(String url) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHeader(String name, String value) {
|
||||||
|
connection.setRequestProperty(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getHeader(String name) {
|
||||||
|
return connection.getRequestProperty(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getAllHeaders() {
|
||||||
|
Map<String, List<String>> origHeaders = connection.getRequestProperties();
|
||||||
|
Map<String, String> headers = new HashMap<String, String>(origHeaders.size());
|
||||||
|
for (String name : origHeaders.keySet()) {
|
||||||
|
List<String> values = origHeaders.get(name);
|
||||||
|
if (!values.isEmpty()) {
|
||||||
|
headers.put(name, values.get(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputStream getMessagePayload() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContentType() {
|
||||||
|
return connection.getRequestProperty("Content-Type");
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpURLConnection unwrap() {
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package org.xbib.net.oauth;
|
||||||
|
|
||||||
|
import org.xbib.net.http.HttpResponse;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
|
||||||
|
public class HttpURLConnectionResponseAdapter implements HttpResponse {
|
||||||
|
|
||||||
|
private HttpURLConnection connection;
|
||||||
|
|
||||||
|
public HttpURLConnectionResponseAdapter(HttpURLConnection connection) {
|
||||||
|
this.connection = connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputStream getContent() {
|
||||||
|
try {
|
||||||
|
return connection.getInputStream();
|
||||||
|
} catch (IOException e) {
|
||||||
|
return connection.getErrorStream();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getStatusCode() throws IOException {
|
||||||
|
return connection.getResponseCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getReasonPhrase() throws Exception {
|
||||||
|
return connection.getResponseMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object unwrap() {
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
}
|
285
net-oauth/src/main/java/org/xbib/net/oauth/OAuth.java
Normal file
285
net-oauth/src/main/java/org/xbib/net/oauth/OAuth.java
Normal file
|
@ -0,0 +1,285 @@
|
||||||
|
package org.xbib.net.oauth;
|
||||||
|
|
||||||
|
import org.xbib.net.PercentDecoder;
|
||||||
|
import org.xbib.net.PercentEncoder;
|
||||||
|
import org.xbib.net.PercentEncoders;
|
||||||
|
import org.xbib.net.http.HttpParameters;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.charset.MalformedInputException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.charset.UnmappableCharacterException;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class OAuth {
|
||||||
|
|
||||||
|
public static final String VERSION_1_0 = "1.0";
|
||||||
|
|
||||||
|
public static final String ENCODING = "UTF-8";
|
||||||
|
|
||||||
|
public static final String FORM_ENCODED = "application/x-www-form-urlencoded";
|
||||||
|
|
||||||
|
public static final String HTTP_AUTHORIZATION_HEADER = "Authorization";
|
||||||
|
|
||||||
|
public static final String OAUTH_CONSUMER_KEY = "oauth_consumer_key";
|
||||||
|
|
||||||
|
public static final String OAUTH_TOKEN = "oauth_token";
|
||||||
|
|
||||||
|
public static final String OAUTH_TOKEN_SECRET = "oauth_token_secret";
|
||||||
|
|
||||||
|
public static final String OAUTH_SIGNATURE_METHOD = "oauth_signature_method";
|
||||||
|
|
||||||
|
public static final String OAUTH_SIGNATURE = "oauth_signature";
|
||||||
|
|
||||||
|
public static final String OAUTH_TIMESTAMP = "oauth_timestamp";
|
||||||
|
|
||||||
|
public static final String OAUTH_NONCE = "oauth_nonce";
|
||||||
|
|
||||||
|
public static final String OAUTH_VERSION = "oauth_version";
|
||||||
|
|
||||||
|
public static final String OAUTH_CALLBACK = "oauth_callback";
|
||||||
|
|
||||||
|
public static final String OAUTH_CALLBACK_CONFIRMED = "oauth_callback_confirmed";
|
||||||
|
|
||||||
|
public static final String OAUTH_VERIFIER = "oauth_verifier";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pass this value as the callback "url" upon retrieving a request token if
|
||||||
|
* your application cannot receive callbacks (e.g. because it's a desktop
|
||||||
|
* app). This will tell the service provider that verification happens
|
||||||
|
* out-of-band, which basically means that it will generate a PIN code (the
|
||||||
|
* OAuth verifier) and display that to your user. You must obtain this code
|
||||||
|
* from your user and pass it to
|
||||||
|
* {@link OAuthProvider#retrieveAccessToken(OAuthConsumer, String, String...)} in order
|
||||||
|
* to complete the token handshake.
|
||||||
|
*/
|
||||||
|
public static final String OUT_OF_BAND = "oob";
|
||||||
|
|
||||||
|
public static final PercentEncoder percentEncoder = PercentEncoders.getQueryEncoder(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
public static final PercentDecoder percentDecoder = new PercentDecoder();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a x-www-form-urlencoded document containing the given sequence
|
||||||
|
* of name/value pairs. Use OAuth percent encoding (not exactly the encoding
|
||||||
|
* mandated by x-www-form-urlencoded).
|
||||||
|
*/
|
||||||
|
public static <T extends Map.Entry<String, String>> void formEncode(Collection<T> parameters,
|
||||||
|
OutputStream into) throws IOException {
|
||||||
|
if (parameters != null) {
|
||||||
|
boolean first = true;
|
||||||
|
for (Map.Entry<String, String> entry : parameters) {
|
||||||
|
if (first) {
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
into.write('&');
|
||||||
|
}
|
||||||
|
into.write(percentEncoder.encode(safeToString(entry.getKey())).getBytes());
|
||||||
|
into.write('=');
|
||||||
|
into.write(percentEncoder.encode(safeToString(entry.getValue())).getBytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a x-www-form-urlencoded document containing the given sequence
|
||||||
|
* of name/value pairs. Use OAuth percent encoding (not exactly the encoding
|
||||||
|
* mandated by x-www-form-urlencoded).
|
||||||
|
*/
|
||||||
|
public static <T extends Map.Entry<String, String>> String formEncode(Collection<T> parameters)
|
||||||
|
throws IOException {
|
||||||
|
ByteArrayOutputStream b = new ByteArrayOutputStream();
|
||||||
|
formEncode(parameters, b);
|
||||||
|
return new String(b.toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a form-urlencoded document.
|
||||||
|
*/
|
||||||
|
public static HttpParameters decodeForm(String form)
|
||||||
|
throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
HttpParameters params = new HttpParameters();
|
||||||
|
if (isEmpty(form)) {
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
for (String nvp : form.split("\\&")) {
|
||||||
|
int equals = nvp.indexOf('=');
|
||||||
|
String name;
|
||||||
|
String value;
|
||||||
|
if (equals < 0) {
|
||||||
|
name = percentDecoder.decode(nvp);
|
||||||
|
value = null;
|
||||||
|
} else {
|
||||||
|
name = percentDecoder.decode(nvp.substring(0, equals));
|
||||||
|
value = percentDecoder.decode(nvp.substring(equals + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
params.put(name, value);
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HttpParameters decodeForm(InputStream content)
|
||||||
|
throws IOException {
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(
|
||||||
|
content));
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
String line = reader.readLine();
|
||||||
|
while (line != null) {
|
||||||
|
sb.append(line);
|
||||||
|
line = reader.readLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
return decodeForm(sb.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a Map containing a copy of the given parameters. If several
|
||||||
|
* parameters have the same name, the Map will contain the first value,
|
||||||
|
* only.
|
||||||
|
*/
|
||||||
|
public static <T extends Map.Entry<String, String>> Map<String, String> toMap(Collection<T> from) {
|
||||||
|
HashMap<String, String> map = new HashMap<String, String>();
|
||||||
|
if (from != null) {
|
||||||
|
for (Map.Entry<String, String> entry : from) {
|
||||||
|
String key = entry.getKey();
|
||||||
|
if (!map.containsKey(key)) {
|
||||||
|
map.put(key, entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String safeToString(Object from) {
|
||||||
|
return (from == null) ? null : from.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isEmpty(String str) {
|
||||||
|
return (str == null) || (str.length() == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a list of key/value pairs to the given URL, e.g.:
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* String url = OAuth.addQueryParameters("http://example.com?a=1", b, 2, c, 3);
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* which yields:
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* http://example.com?a=1&b=2&c=3
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* All parameters will be encoded according to OAuth's percent encoding
|
||||||
|
* rules.
|
||||||
|
*
|
||||||
|
* @param url
|
||||||
|
* the URL
|
||||||
|
* @param kvPairs
|
||||||
|
* the list of key/value pairs
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static String addQueryParameters(String url, String... kvPairs)
|
||||||
|
throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
String queryDelim = url.contains("?") ? "&" : "?";
|
||||||
|
StringBuilder sb = new StringBuilder(url + queryDelim);
|
||||||
|
for (int i = 0; i < kvPairs.length; i += 2) {
|
||||||
|
if (i > 0) {
|
||||||
|
sb.append("&");
|
||||||
|
}
|
||||||
|
sb.append(percentEncoder.encode(kvPairs[i])).append("=")
|
||||||
|
.append(percentEncoder.encode(kvPairs[i + 1]));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String addQueryParameters(String url, Map<String, String> params)
|
||||||
|
throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
String[] kvPairs = new String[params.size() * 2];
|
||||||
|
int idx = 0;
|
||||||
|
for (String key : params.keySet()) {
|
||||||
|
kvPairs[idx] = key;
|
||||||
|
kvPairs[idx + 1] = params.get(key);
|
||||||
|
idx += 2;
|
||||||
|
}
|
||||||
|
return addQueryParameters(url, kvPairs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String addQueryString(String url, String queryString) {
|
||||||
|
String queryDelim = url.contains("?") ? "&" : "?";
|
||||||
|
return url + queryDelim + queryString;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds an OAuth header from the given list of header fields. All
|
||||||
|
* parameters starting in 'oauth_*' will be percent encoded.
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* String authHeader = OAuth.prepareOAuthHeader("realm", "http://example.com", "oauth_token", "x%y");
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* which yields:
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* OAuth realm="http://example.com", oauth_token="x%25y"
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @param kvPairs
|
||||||
|
* the list of key/value pairs
|
||||||
|
* @return a string eligible to be used as an OAuth HTTP Authorization
|
||||||
|
* header.
|
||||||
|
*/
|
||||||
|
public static String prepareOAuthHeader(String... kvPairs)
|
||||||
|
throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
StringBuilder sb = new StringBuilder("OAuth ");
|
||||||
|
for (int i = 0; i < kvPairs.length; i += 2) {
|
||||||
|
if (i > 0) {
|
||||||
|
sb.append(", ");
|
||||||
|
}
|
||||||
|
boolean isOAuthElem = kvPairs[i].startsWith("oauth_")
|
||||||
|
|| kvPairs[i].startsWith("x_oauth_");
|
||||||
|
String value = isOAuthElem ? percentEncoder.encode(kvPairs[i + 1]) : kvPairs[i + 1];
|
||||||
|
sb.append(percentEncoder.encode(kvPairs[i])).append("=\"").append(value).append("\"");
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HttpParameters oauthHeaderToParamsMap(String oauthHeader)
|
||||||
|
throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
HttpParameters params = new HttpParameters();
|
||||||
|
if (oauthHeader == null || !oauthHeader.startsWith("OAuth ")) {
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
String[] elements = oauthHeader.substring("OAuth ".length()).split(",");
|
||||||
|
for (String keyValuePair : elements) {
|
||||||
|
String[] keyValue = keyValuePair.split("=");
|
||||||
|
params.put(keyValue[0].trim(), keyValue[1].replace("\"", "").trim());
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to concatenate a parameter and its value to a pair that can
|
||||||
|
* be used in an HTTP header. This method percent encodes both parts before
|
||||||
|
* joining them.
|
||||||
|
*
|
||||||
|
* @param name
|
||||||
|
* the OAuth parameter name, e.g. oauth_token
|
||||||
|
* @param value
|
||||||
|
* the OAuth parameter value, e.g. 'hello oauth'
|
||||||
|
* @return a name/value pair, e.g. oauth_token="hello%20oauth"
|
||||||
|
*/
|
||||||
|
public static String toHeaderElement(String name, String value)
|
||||||
|
throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
return percentEncoder.encode(name) + "=\"" + percentEncoder.encode(value) + "\"";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package org.xbib.net.oauth;
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public class OAuthCommunicationException extends OAuthException {
|
||||||
|
|
||||||
|
private String responseBody;
|
||||||
|
|
||||||
|
public OAuthCommunicationException(Exception cause) {
|
||||||
|
super("Communication with the service provider failed: "
|
||||||
|
+ cause.getLocalizedMessage(), cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OAuthCommunicationException(String message, String responseBody) {
|
||||||
|
super(message);
|
||||||
|
this.responseBody = responseBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getResponseBody() {
|
||||||
|
return responseBody;
|
||||||
|
}
|
||||||
|
}
|
157
net-oauth/src/main/java/org/xbib/net/oauth/OAuthConsumer.java
Normal file
157
net-oauth/src/main/java/org/xbib/net/oauth/OAuthConsumer.java
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
package org.xbib.net.oauth;
|
||||||
|
|
||||||
|
import org.xbib.net.http.HttpParameters;
|
||||||
|
import org.xbib.net.http.HttpRequest;
|
||||||
|
import org.xbib.net.oauth.sign.OAuthMessageSigner;
|
||||||
|
import org.xbib.net.oauth.sign.SigningStrategy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* Exposes a simple interface to sign HTTP requests using a given OAuth token
|
||||||
|
* and secret. Refer to {@link OAuthProvider} how to retrieve a valid token and
|
||||||
|
* token secret.
|
||||||
|
* </p>
|
||||||
|
* HTTP messages are signed as follows:
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* // exchange the arguments with the actual token/secret pair
|
||||||
|
* OAuthConsumer consumer = new DefaultOAuthConsumer("1234", "5678");
|
||||||
|
* URL url = new URL("http://example.com/protected.xml");
|
||||||
|
* HttpURLConnection request = (HttpURLConnection) url.openConnection();
|
||||||
|
* consumer.sign(request);
|
||||||
|
* request.connect();
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public interface OAuthConsumer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the message signer that should be used to generate the OAuth
|
||||||
|
* signature.
|
||||||
|
*
|
||||||
|
* @param messageSigner
|
||||||
|
* the signer
|
||||||
|
*/
|
||||||
|
void setMessageSigner(OAuthMessageSigner messageSigner);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows you to add parameters (typically OAuth parameters such as
|
||||||
|
* oauth_callback or oauth_verifier) which will go directly into the signer,
|
||||||
|
* i.e. you don't have to put them into the request first. The consumer's
|
||||||
|
* signing strategy will then take care of writing them to the
|
||||||
|
* correct part of the request before it is sent. This is useful if you want
|
||||||
|
* to pre-set custom OAuth parameters. Note that these parameters are
|
||||||
|
* expected to already be percent encoded -- they will be simply merged
|
||||||
|
* as-is. <b>BE CAREFUL WITH THIS METHOD! Your service provider may decide
|
||||||
|
* to ignore any non-standard OAuth params when computing the signature.</b>
|
||||||
|
*
|
||||||
|
* @param additionalParameters
|
||||||
|
* the parameters
|
||||||
|
*/
|
||||||
|
void setAdditionalParameters(HttpParameters additionalParameters);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines which strategy should be used to write a signature to an HTTP
|
||||||
|
* request.
|
||||||
|
*
|
||||||
|
* @param signingStrategy
|
||||||
|
* the strategy
|
||||||
|
*/
|
||||||
|
void setSigningStrategy(SigningStrategy signingStrategy);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* Causes the consumer to always include the oauth_token parameter to be
|
||||||
|
* sent, even if blank. If you're seeing 401s during calls to
|
||||||
|
* {@link OAuthProvider#retrieveRequestToken}, try setting this to true.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param enable
|
||||||
|
* true or false
|
||||||
|
*/
|
||||||
|
void setSendEmptyTokens(boolean enable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signs the given HTTP request by writing an OAuth signature (and other
|
||||||
|
* required OAuth parameters) to it. Where these parameters are written
|
||||||
|
* depends on the current {@link SigningStrategy}.
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* the request to sign
|
||||||
|
* @return the request object passed as an argument
|
||||||
|
* @throws OAuthMessageSignerException
|
||||||
|
* @throws OAuthExpectationFailedException
|
||||||
|
* @throws OAuthCommunicationException
|
||||||
|
*/
|
||||||
|
HttpRequest sign(HttpRequest request) throws OAuthMessageSignerException,
|
||||||
|
OAuthExpectationFailedException, OAuthCommunicationException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* Signs the given HTTP request by writing an OAuth signature (and other
|
||||||
|
* required OAuth parameters) to it. Where these parameters are written
|
||||||
|
* depends on the current {@link SigningStrategy}.
|
||||||
|
* </p>
|
||||||
|
* This method accepts HTTP library specific request objects; the consumer
|
||||||
|
* implementation must ensure that only those request types are passed which
|
||||||
|
* it supports.
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* the request to sign
|
||||||
|
* @return the request object passed as an argument
|
||||||
|
* @throws OAuthMessageSignerException
|
||||||
|
* @throws OAuthExpectationFailedException
|
||||||
|
* @throws OAuthCommunicationException
|
||||||
|
*/
|
||||||
|
HttpRequest sign(Object request) throws OAuthMessageSignerException,
|
||||||
|
OAuthExpectationFailedException, OAuthCommunicationException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "Signs" the given URL by appending all OAuth parameters to it which are
|
||||||
|
* required for message signing. The assumed HTTP method is GET.
|
||||||
|
* Essentially, this is equivalent to signing an HTTP GET request, but it
|
||||||
|
* can be useful if your application requires clickable links to protected
|
||||||
|
* resources, i.e. when your application does not have access to the actual
|
||||||
|
* request that is being sent.
|
||||||
|
*
|
||||||
|
* @param url
|
||||||
|
* the input URL. May have query parameters.
|
||||||
|
* @return the input URL, with all necessary OAuth parameters attached as a
|
||||||
|
* query string. Existing query parameters are preserved.
|
||||||
|
* @throws OAuthMessageSignerException
|
||||||
|
* @throws OAuthExpectationFailedException
|
||||||
|
* @throws OAuthCommunicationException
|
||||||
|
*/
|
||||||
|
String sign(String url) throws OAuthMessageSignerException,
|
||||||
|
OAuthExpectationFailedException, OAuthCommunicationException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the OAuth token and token secret used for message signing.
|
||||||
|
*
|
||||||
|
* @param token
|
||||||
|
* the token
|
||||||
|
* @param tokenSecret
|
||||||
|
* the token secret
|
||||||
|
*/
|
||||||
|
void setTokenWithSecret(String token, String tokenSecret);
|
||||||
|
|
||||||
|
String getToken();
|
||||||
|
|
||||||
|
String getTokenSecret();
|
||||||
|
|
||||||
|
String getConsumerKey();
|
||||||
|
|
||||||
|
String getConsumerSecret();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all parameters collected from the HTTP request during message
|
||||||
|
* signing (this means the return value may be NULL before a call to
|
||||||
|
* {@link #sign}), plus all required OAuth parameters that were added
|
||||||
|
* because the request didn't contain them beforehand. In other words, this
|
||||||
|
* is the exact set of parameters that were used for creating the message
|
||||||
|
* signature.
|
||||||
|
*
|
||||||
|
* @return the request parameters used for message signing
|
||||||
|
*/
|
||||||
|
HttpParameters getRequestParameters();
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package org.xbib.net.oauth;
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public abstract class OAuthException extends Exception {
|
||||||
|
|
||||||
|
public OAuthException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OAuthException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OAuthException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package org.xbib.net.oauth;
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public class OAuthExpectationFailedException extends OAuthException {
|
||||||
|
|
||||||
|
public OAuthExpectationFailedException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package org.xbib.net.oauth;
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public class OAuthMessageSignerException extends OAuthException {
|
||||||
|
|
||||||
|
public OAuthMessageSignerException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OAuthMessageSignerException(Exception cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package org.xbib.net.oauth;
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public class OAuthNotAuthorizedException extends OAuthException {
|
||||||
|
|
||||||
|
private static final String ERROR = "Authorization failed (server replied with a 401). "
|
||||||
|
+ "This can happen if the consumer key was not correct or "
|
||||||
|
+ "the signatures did not match.";
|
||||||
|
|
||||||
|
private String responseBody;
|
||||||
|
|
||||||
|
public OAuthNotAuthorizedException() {
|
||||||
|
super(ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OAuthNotAuthorizedException(String responseBody) {
|
||||||
|
super(ERROR);
|
||||||
|
this.responseBody = responseBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getResponseBody() {
|
||||||
|
return responseBody;
|
||||||
|
}
|
||||||
|
}
|
206
net-oauth/src/main/java/org/xbib/net/oauth/OAuthProvider.java
Normal file
206
net-oauth/src/main/java/org/xbib/net/oauth/OAuthProvider.java
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
package org.xbib.net.oauth;
|
||||||
|
|
||||||
|
import org.xbib.net.http.HttpParameters;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* Supplies an interface that can be used to retrieve request and access tokens
|
||||||
|
* from an OAuth 1.0(a) service provider. A provider object requires an
|
||||||
|
* {@link OAuthConsumer} to sign the token request message; after a token has
|
||||||
|
* been retrieved, the consumer is automatically updated with the token and the
|
||||||
|
* corresponding secret.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* To initiate the token exchange, create a new provider instance and configure
|
||||||
|
* it with the URLs the service provider exposes for requesting tokens and
|
||||||
|
* resource authorization, e.g.:
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* OAuthProvider provider = new DefaultOAuthProvider("http://twitter.com/oauth/request_token",
|
||||||
|
* "http://twitter.com/oauth/access_token", "http://twitter.com/oauth/authorize");
|
||||||
|
* </pre>
|
||||||
|
* <p>
|
||||||
|
* Depending on the HTTP library you use, you may need a different provider
|
||||||
|
* type, refer to the website documentation for how to do that.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* To receive a request token which the user must authorize, you invoke it using
|
||||||
|
* a consumer instance and a callback URL:
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* String url = provider.retrieveRequestToken(consumer, "http://www.example.com/callback");
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* That url must be opened in a Web browser, where the user can grant access to
|
||||||
|
* the resources in question. If that succeeds, the service provider will
|
||||||
|
* redirect to the callback URL and append the blessed request token.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* That token must now be exchanged for an access token, as such:
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* provider.retrieveAccessToken(consumer, nullOrVerifierCode);
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* where nullOrVerifierCode is either null if your provided a callback URL in
|
||||||
|
* the previous step, or the pin code issued by the service provider to the user
|
||||||
|
* if the request was out-of-band (cf. {@link OAuth#OUT_OF_BAND}.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* The consumer used during token handshakes is now ready for signing.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @see OAuthProviderListener
|
||||||
|
*/
|
||||||
|
public interface OAuthProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queries the service provider for a request token.
|
||||||
|
* <p>
|
||||||
|
* <b>Pre-conditions:</b> the given {@link OAuthConsumer} must have a valid
|
||||||
|
* consumer key and consumer secret already set.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* <b>Post-conditions:</b> the given {@link OAuthConsumer} will have an
|
||||||
|
* unauthorized request token and token secret set.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param consumer
|
||||||
|
* the {@link OAuthConsumer} that should be used to sign the request
|
||||||
|
* @param callbackUrl
|
||||||
|
* Pass an actual URL if your app can receive callbacks and you want
|
||||||
|
* to get informed about the result of the authorization process.
|
||||||
|
* Pass OUT_OF_BAND if the service provider implements
|
||||||
|
* OAuth 1.0a and your app cannot receive callbacks. Pass null if the
|
||||||
|
* service provider implements OAuth 1.0 and your app cannot receive
|
||||||
|
* callbacks. Please note that some services (among them Twitter)
|
||||||
|
* will fail authorization if you pass a callback URL but register
|
||||||
|
* your application as a desktop app (which would only be able to
|
||||||
|
* handle OOB requests).
|
||||||
|
* @param customOAuthParams
|
||||||
|
* you can pass custom OAuth parameters here which will go directly
|
||||||
|
* into the signer, i.e. you don't have to put them into the request
|
||||||
|
* first. This is useful for pre-setting OAuth params for signing.
|
||||||
|
* Pass them sequentially in key/value order.
|
||||||
|
* @return The URL to which the user must be sent in order to authorize the
|
||||||
|
* consumer. It includes the unauthorized request token (and in the
|
||||||
|
* case of OAuth 1.0, the callback URL -- 1.0a clients send along
|
||||||
|
* with the token request).
|
||||||
|
* @throws OAuthMessageSignerException
|
||||||
|
* if signing the request failed
|
||||||
|
* @throws OAuthNotAuthorizedException
|
||||||
|
* if the service provider rejected the consumer
|
||||||
|
* @throws OAuthExpectationFailedException
|
||||||
|
* if required parameters were not correctly set by the consumer or
|
||||||
|
* service provider
|
||||||
|
* @throws OAuthCommunicationException
|
||||||
|
* if server communication failed
|
||||||
|
*/
|
||||||
|
String retrieveRequestToken(OAuthConsumer consumer, String callbackUrl,
|
||||||
|
String... customOAuthParams) throws OAuthMessageSignerException,
|
||||||
|
OAuthNotAuthorizedException, OAuthExpectationFailedException,
|
||||||
|
OAuthCommunicationException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queries the service provider for an access token.
|
||||||
|
* <p>
|
||||||
|
* <b>Pre-conditions:</b> the given {@link OAuthConsumer} must have a valid
|
||||||
|
* consumer key, consumer secret, authorized request token and token secret
|
||||||
|
* already set.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* <b>Post-conditions:</b> the given {@link OAuthConsumer} will have an
|
||||||
|
* access token and token secret set.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param consumer
|
||||||
|
* the {@link OAuthConsumer} that should be used to sign the request
|
||||||
|
* @param oauthVerifier
|
||||||
|
* <b>NOTE: Only applies to service providers implementing OAuth
|
||||||
|
* 1.0a. Set to null if the service provider is still using OAuth
|
||||||
|
* 1.0.</b> The verification code issued by the service provider
|
||||||
|
* after the the user has granted the consumer authorization. If the
|
||||||
|
* callback method provided in the previous step was
|
||||||
|
* OUT_OF_BAND, then you must ask the user for this
|
||||||
|
* value. If your app has received a callback, the verfication code
|
||||||
|
* was passed as part of that request instead.
|
||||||
|
* @param customOAuthParams
|
||||||
|
* you can pass custom OAuth parameters here which will go directly
|
||||||
|
* into the signer, i.e. you don't have to put them into the request
|
||||||
|
* first. This is useful for pre-setting OAuth params for signing.
|
||||||
|
* Pass them sequentially in key/value order.
|
||||||
|
* @throws OAuthMessageSignerException
|
||||||
|
* if signing the request failed
|
||||||
|
* @throws OAuthNotAuthorizedException
|
||||||
|
* if the service provider rejected the consumer
|
||||||
|
* @throws OAuthExpectationFailedException
|
||||||
|
* if required parameters were not correctly set by the consumer or
|
||||||
|
* service provider
|
||||||
|
* @throws OAuthCommunicationException
|
||||||
|
* if server communication failed
|
||||||
|
*/
|
||||||
|
void retrieveAccessToken(OAuthConsumer consumer, String oauthVerifier,
|
||||||
|
String... customOAuthParams) throws OAuthMessageSignerException,
|
||||||
|
OAuthNotAuthorizedException, OAuthExpectationFailedException,
|
||||||
|
OAuthCommunicationException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Any additional non-OAuth parameters returned in the response body of a
|
||||||
|
* token request can be obtained through this method. These parameters will
|
||||||
|
* be preserved until the next token request is issued. The return value is
|
||||||
|
* never null.
|
||||||
|
*/
|
||||||
|
HttpParameters getResponseParameters();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subclasses must use this setter to preserve any non-OAuth query
|
||||||
|
* parameters contained in the server response. It's the caller's
|
||||||
|
* responsibility that any OAuth parameters be removed beforehand.
|
||||||
|
*
|
||||||
|
* @param parameters
|
||||||
|
* the map of query parameters served by the service provider in the
|
||||||
|
* token response
|
||||||
|
*/
|
||||||
|
void setResponseParameters(HttpParameters parameters);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param isOAuth10aProvider
|
||||||
|
* set to true if the service provider supports OAuth 1.0a. Note that
|
||||||
|
* you need only call this method if you reconstruct a provider
|
||||||
|
* object in between calls to retrieveRequestToken() and
|
||||||
|
* retrieveAccessToken() (i.e. if the object state isn't preserved).
|
||||||
|
* If instead those two methods are called on the same provider
|
||||||
|
* instance, this flag will be deducted automatically based on the
|
||||||
|
* server response during retrieveRequestToken(), so you can simply
|
||||||
|
* ignore this method.
|
||||||
|
*/
|
||||||
|
void setOAuth10a(boolean isOAuth10aProvider);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if the service provider supports OAuth 1.0a. Note that the
|
||||||
|
* value returned here is only meaningful after you have already
|
||||||
|
* performed the token handshake, otherwise there is no way to
|
||||||
|
* determine what version of the OAuth protocol the service provider
|
||||||
|
* implements.
|
||||||
|
*/
|
||||||
|
boolean isOAuth10a();
|
||||||
|
|
||||||
|
String getRequestTokenEndpointUrl();
|
||||||
|
|
||||||
|
String getAccessTokenEndpointUrl();
|
||||||
|
|
||||||
|
String getAuthorizationWebsiteUrl();
|
||||||
|
|
||||||
|
void setListener(OAuthProviderListener listener);
|
||||||
|
|
||||||
|
void removeListener(OAuthProviderListener listener);
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package org.xbib.net.oauth;
|
||||||
|
|
||||||
|
import org.xbib.net.http.HttpRequest;
|
||||||
|
import org.xbib.net.http.HttpResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides hooks into the token request handling procedure executed by
|
||||||
|
* {@link OAuthProvider}.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public interface OAuthProviderListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called after the request has been created and default headers added, but
|
||||||
|
* before the request has been signed.
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* the request to be sent
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
void prepareRequest(HttpRequest request) throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called after the request has been signed, but before it's being sent.
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* the request to be sent
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
void prepareSubmission(HttpRequest request) throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the server response has been received. You can implement this
|
||||||
|
* to manually handle the response data.
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* the request that was sent
|
||||||
|
* @param response
|
||||||
|
* the response that was received
|
||||||
|
* @return returning true means you have handled the response, and the
|
||||||
|
* provider will return immediately. Return false to let the event
|
||||||
|
* propagate and let the provider execute its default response
|
||||||
|
* handling.
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
boolean onResponseReceived(HttpRequest request, HttpResponse response) throws Exception;
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
package org.xbib.net.oauth.sign;
|
||||||
|
|
||||||
|
import org.xbib.net.http.HttpParameters;
|
||||||
|
import org.xbib.net.http.HttpRequest;
|
||||||
|
import org.xbib.net.oauth.OAuth;
|
||||||
|
|
||||||
|
import java.nio.charset.MalformedInputException;
|
||||||
|
import java.nio.charset.UnmappableCharacterException;
|
||||||
|
import java.util.Iterator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes to the HTTP Authorization header field.
|
||||||
|
*/
|
||||||
|
public class AuthorizationHeaderSigningStrategy implements SigningStrategy {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String writeSignature(String signature, HttpRequest request,
|
||||||
|
HttpParameters requestParameters) throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("OAuth ");
|
||||||
|
// add the realm parameter, if any
|
||||||
|
if (requestParameters.containsKey("realm")) {
|
||||||
|
sb.append(requestParameters.getAsHeaderElement("realm"));
|
||||||
|
sb.append(", ");
|
||||||
|
}
|
||||||
|
// add all (x_)oauth parameters
|
||||||
|
HttpParameters oauthParams = requestParameters.getOAuthParameters();
|
||||||
|
oauthParams.put(OAuth.OAUTH_SIGNATURE, signature, true);
|
||||||
|
Iterator<String> iterator = oauthParams.keySet().iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
String key = iterator.next();
|
||||||
|
sb.append(oauthParams.getAsHeaderElement(key));
|
||||||
|
if (iterator.hasNext()) {
|
||||||
|
sb.append(", ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String header = sb.toString();
|
||||||
|
request.setHeader(OAuth.HTTP_AUTHORIZATION_HEADER, header);
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package org.xbib.net.oauth.sign;
|
||||||
|
|
||||||
|
import org.xbib.net.http.HttpParameters;
|
||||||
|
import org.xbib.net.http.HttpRequest;
|
||||||
|
import org.xbib.net.oauth.OAuth;
|
||||||
|
import org.xbib.net.oauth.OAuthMessageSignerException;
|
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.nio.charset.MalformedInputException;
|
||||||
|
import java.nio.charset.UnmappableCharacterException;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public class HmacSha1MessageSigner extends OAuthMessageSigner {
|
||||||
|
|
||||||
|
private static final String MAC_NAME = "HmacSHA1";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getSignatureMethod() {
|
||||||
|
return "HMAC-SHA1";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String sign(HttpRequest request, HttpParameters requestParams)
|
||||||
|
throws OAuthMessageSignerException {
|
||||||
|
try {
|
||||||
|
String keyString = OAuth.percentEncoder.encode(getConsumerSecret()) + '&'
|
||||||
|
+ OAuth.percentEncoder.encode(getTokenSecret());
|
||||||
|
byte[] keyBytes = keyString.getBytes(OAuth.ENCODING);
|
||||||
|
SecretKey key = new SecretKeySpec(keyBytes, MAC_NAME);
|
||||||
|
Mac mac = Mac.getInstance(MAC_NAME);
|
||||||
|
mac.init(key);
|
||||||
|
String sbs = new SignatureBaseString(request, requestParams).generate();
|
||||||
|
byte[] text = sbs.getBytes(OAuth.ENCODING);
|
||||||
|
return base64Encode(mac.doFinal(text)).trim();
|
||||||
|
} catch (GeneralSecurityException | UnsupportedEncodingException |
|
||||||
|
MalformedInputException | UnmappableCharacterException e) {
|
||||||
|
throw new OAuthMessageSignerException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package org.xbib.net.oauth.sign;
|
||||||
|
|
||||||
|
import org.xbib.net.http.HttpParameters;
|
||||||
|
import org.xbib.net.http.HttpRequest;
|
||||||
|
import org.xbib.net.oauth.OAuth;
|
||||||
|
import org.xbib.net.oauth.OAuthMessageSignerException;
|
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.nio.charset.MalformedInputException;
|
||||||
|
import java.nio.charset.UnmappableCharacterException;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public class HmacSha256MessageSigner extends OAuthMessageSigner {
|
||||||
|
|
||||||
|
private static final String MAC_NAME = "HmacSHA256";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getSignatureMethod() {
|
||||||
|
return "HMAC-SHA256";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String sign(HttpRequest request, HttpParameters requestParams)
|
||||||
|
throws OAuthMessageSignerException {
|
||||||
|
try {
|
||||||
|
String keyString = OAuth.percentEncoder.encode(getConsumerSecret()) + '&'
|
||||||
|
+ OAuth.percentEncoder.encode(getTokenSecret());
|
||||||
|
byte[] keyBytes = keyString.getBytes(OAuth.ENCODING);
|
||||||
|
SecretKey key = new SecretKeySpec(keyBytes, MAC_NAME);
|
||||||
|
Mac mac = Mac.getInstance(MAC_NAME);
|
||||||
|
mac.init(key);
|
||||||
|
String sbs = new SignatureBaseString(request, requestParams).generate();
|
||||||
|
byte[] text = sbs.getBytes(OAuth.ENCODING);
|
||||||
|
return base64Encode(mac.doFinal(text)).trim();
|
||||||
|
} catch (GeneralSecurityException | UnsupportedEncodingException |
|
||||||
|
MalformedInputException| UnmappableCharacterException e) {
|
||||||
|
throw new OAuthMessageSignerException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
package org.xbib.net.oauth.sign;
|
||||||
|
|
||||||
|
import org.xbib.net.http.HttpParameters;
|
||||||
|
import org.xbib.net.http.HttpRequest;
|
||||||
|
import org.xbib.net.oauth.OAuthMessageSignerException;
|
||||||
|
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
|
||||||
|
public abstract class OAuthMessageSigner {
|
||||||
|
|
||||||
|
private Base64.Encoder base64Encoder;
|
||||||
|
|
||||||
|
private Base64.Decoder base64Decoder;
|
||||||
|
|
||||||
|
private String consumerSecret;
|
||||||
|
|
||||||
|
private String tokenSecret;
|
||||||
|
|
||||||
|
public OAuthMessageSigner() {
|
||||||
|
this.base64Encoder = Base64.getEncoder();
|
||||||
|
this.base64Decoder = Base64.getDecoder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract String sign(HttpRequest request, HttpParameters requestParameters)
|
||||||
|
throws OAuthMessageSignerException;
|
||||||
|
|
||||||
|
public abstract String getSignatureMethod();
|
||||||
|
|
||||||
|
public String getConsumerSecret() {
|
||||||
|
return consumerSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTokenSecret() {
|
||||||
|
return tokenSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConsumerSecret(String consumerSecret) {
|
||||||
|
this.consumerSecret = consumerSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTokenSecret(String tokenSecret) {
|
||||||
|
this.tokenSecret = tokenSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected byte[] decodeBase64(String s) {
|
||||||
|
return base64Decoder.decode(s.getBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String base64Encode(byte[] b) {
|
||||||
|
return new String(base64Encoder.encode(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package org.xbib.net.oauth.sign;
|
||||||
|
|
||||||
|
import org.xbib.net.http.HttpParameters;
|
||||||
|
import org.xbib.net.http.HttpRequest;
|
||||||
|
import org.xbib.net.oauth.OAuth;
|
||||||
|
import org.xbib.net.oauth.OAuthMessageSignerException;
|
||||||
|
|
||||||
|
import java.nio.charset.MalformedInputException;
|
||||||
|
import java.nio.charset.UnmappableCharacterException;
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public class PlainTextMessageSigner extends OAuthMessageSigner {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getSignatureMethod() {
|
||||||
|
return "PLAINTEXT";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String sign(HttpRequest request, HttpParameters requestParams)
|
||||||
|
throws OAuthMessageSignerException {
|
||||||
|
try {
|
||||||
|
return OAuth.percentEncoder.encode(getConsumerSecret()) + '&'
|
||||||
|
+ OAuth.percentEncoder.encode(getTokenSecret());
|
||||||
|
} catch (MalformedInputException | UnmappableCharacterException e) {
|
||||||
|
throw new OAuthMessageSignerException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package org.xbib.net.oauth.sign;
|
||||||
|
|
||||||
|
import org.xbib.net.http.HttpParameters;
|
||||||
|
import org.xbib.net.http.HttpRequest;
|
||||||
|
import org.xbib.net.oauth.OAuth;
|
||||||
|
|
||||||
|
import java.nio.charset.MalformedInputException;
|
||||||
|
import java.nio.charset.UnmappableCharacterException;
|
||||||
|
import java.util.Iterator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes to a URL query string. <strong>Note that this currently ONLY works
|
||||||
|
* when signing a URL directly, not with HTTP request objects.</strong> That's
|
||||||
|
* because most HTTP request implementations do not allow the client to change
|
||||||
|
* the URL once the request has been instantiated, so there is no way to append
|
||||||
|
* parameters to it.
|
||||||
|
*/
|
||||||
|
public class QueryStringSigningStrategy implements SigningStrategy {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String writeSignature(String signature, HttpRequest request,
|
||||||
|
HttpParameters requestParameters)
|
||||||
|
throws MalformedInputException, UnmappableCharacterException {
|
||||||
|
// add all (x_)oauth parameters
|
||||||
|
HttpParameters oauthParams = requestParameters.getOAuthParameters();
|
||||||
|
oauthParams.put(OAuth.OAUTH_SIGNATURE, signature, true);
|
||||||
|
Iterator<String> iterator = oauthParams.keySet().iterator();
|
||||||
|
// add the first query parameter (we always have at least the signature)
|
||||||
|
String firstKey = iterator.next();
|
||||||
|
StringBuilder sb = new StringBuilder(OAuth.addQueryString(request.getRequestUrl(),
|
||||||
|
oauthParams.getAsQueryString(firstKey)));
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
sb.append("&");
|
||||||
|
String key = iterator.next();
|
||||||
|
sb.append(oauthParams.getAsQueryString(key));
|
||||||
|
}
|
||||||
|
String signedUrl = sb.toString();
|
||||||
|
request.setRequestUrl(signedUrl);
|
||||||
|
return signedUrl;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
package org.xbib.net.oauth.sign;
|
||||||
|
|
||||||
|
import org.xbib.net.http.HttpParameters;
|
||||||
|
import org.xbib.net.http.HttpRequest;
|
||||||
|
import org.xbib.net.oauth.OAuth;
|
||||||
|
import org.xbib.net.oauth.OAuthMessageSignerException;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.Iterator;
|
||||||
|
|
||||||
|
public class SignatureBaseString {
|
||||||
|
|
||||||
|
private final HttpRequest request;
|
||||||
|
|
||||||
|
private final HttpParameters requestParameters;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance that will operate on the given request
|
||||||
|
* object and parameter set.
|
||||||
|
*
|
||||||
|
* @param request the HTTP request
|
||||||
|
* @param requestParameters the set of request parameters from the Authorization header, query
|
||||||
|
* string and form body
|
||||||
|
*/
|
||||||
|
public SignatureBaseString(HttpRequest request, HttpParameters requestParameters) {
|
||||||
|
this.request = request;
|
||||||
|
this.requestParameters = requestParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the signature base string from the data this instance was
|
||||||
|
* configured with.
|
||||||
|
*
|
||||||
|
* @return the signature base string
|
||||||
|
* @throws OAuthMessageSignerException
|
||||||
|
*/
|
||||||
|
public String generate() throws OAuthMessageSignerException {
|
||||||
|
try {
|
||||||
|
String normalizedUrl = normalizeRequestUrl();
|
||||||
|
String normalizedParams = normalizeRequestParameters();
|
||||||
|
return request.getMethod() + '&' + OAuth.percentEncoder.encode(normalizedUrl) + '&'
|
||||||
|
+ OAuth.percentEncoder.encode(normalizedParams);
|
||||||
|
} catch (URISyntaxException | IOException e) {
|
||||||
|
throw new OAuthMessageSignerException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String normalizeRequestUrl() throws URISyntaxException {
|
||||||
|
URI uri = new URI(request.getRequestUrl());
|
||||||
|
String scheme = uri.getScheme().toLowerCase();
|
||||||
|
String authority = uri.getAuthority().toLowerCase();
|
||||||
|
boolean dropPort = (scheme.equals("http") && uri.getPort() == 80)
|
||||||
|
|| (scheme.equals("https") && uri.getPort() == 443);
|
||||||
|
if (dropPort) {
|
||||||
|
// find the last : in the authority
|
||||||
|
int index = authority.lastIndexOf(":");
|
||||||
|
if (index >= 0) {
|
||||||
|
authority = authority.substring(0, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String path = uri.getRawPath();
|
||||||
|
if (path == null || path.length() <= 0) {
|
||||||
|
path = "/"; // conforms to RFC 2616 section 3.2.2
|
||||||
|
}
|
||||||
|
// we know that there is no query and no fragment here.
|
||||||
|
return scheme + "://" + authority + path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes the set of request parameters this instance was configured
|
||||||
|
* with, as per OAuth spec section 9.1.1.
|
||||||
|
*
|
||||||
|
* @return the normalized params string
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public String normalizeRequestParameters() throws IOException {
|
||||||
|
if (requestParameters == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
Iterator<String> iter = requestParameters.keySet().iterator();
|
||||||
|
for (int i = 0; iter.hasNext(); i++) {
|
||||||
|
String param = iter.next();
|
||||||
|
if (OAuth.OAUTH_SIGNATURE.equals(param) || "realm".equals(param)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (i > 0) {
|
||||||
|
sb.append("&");
|
||||||
|
}
|
||||||
|
sb.append(requestParameters.getAsQueryString(param, false));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package org.xbib.net.oauth.sign;
|
||||||
|
|
||||||
|
import org.xbib.net.http.HttpParameters;
|
||||||
|
import org.xbib.net.http.HttpRequest;
|
||||||
|
|
||||||
|
import java.nio.charset.MalformedInputException;
|
||||||
|
import java.nio.charset.UnmappableCharacterException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* Defines how an OAuth signature string is written to a request.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* Unlike {@link OAuthMessageSigner}, which is concerned with <i>how</i> to
|
||||||
|
* generate a signature, this class is concered with <i>where</i> to write it
|
||||||
|
* (e.g. HTTP header or query string).
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public interface SigningStrategy {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes an OAuth signature and all remaining required parameters to an
|
||||||
|
* HTTP message.
|
||||||
|
*
|
||||||
|
* @param signature
|
||||||
|
* the signature to write
|
||||||
|
* @param request
|
||||||
|
* the request to sign
|
||||||
|
* @param requestParameters
|
||||||
|
* the request parameters
|
||||||
|
* @return whatever has been written to the request, e.g. an Authorization
|
||||||
|
* header field
|
||||||
|
*/
|
||||||
|
String writeSignature(String signature, HttpRequest request, HttpParameters requestParameters)
|
||||||
|
throws MalformedInputException, UnmappableCharacterException;
|
||||||
|
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query parameters.
|
* Query parameter list, of limited size. Default is 1024 pairs.
|
||||||
*/
|
*/
|
||||||
public class QueryParameters extends ArrayList<QueryParameters.Pair<String, String>> {
|
public class QueryParameters extends ArrayList<QueryParameters.Pair<String, String>> {
|
||||||
|
|
||||||
|
|
|
@ -864,6 +864,12 @@ public class URL implements Comparable<URL> {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder resetQueryParams() {
|
||||||
|
queryParams.clear();
|
||||||
|
query = null;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a query parameter. Query parameters will be encoded in the order added.
|
* Add a query parameter. Query parameters will be encoded in the order added.
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
rootProject.name = name
|
rootProject.name = name
|
||||||
|
|
||||||
include 'net-url'
|
include 'net-url'
|
||||||
|
include 'net-http'
|
||||||
|
|
Loading…
Reference in a new issue