From 532efef6350682fa3eb136ee08d106791deb360a Mon Sep 17 00:00:00 2001 From: Peter Wu Date: Wed, 23 Apr 2014 19:51:17 +0200 Subject: [WIP] API requester abstraction Authentication, etc. is shuffled into Requester. Implementators (currently OAuth and Bearer) should prepare proper requests. --- lib/commons-io-2.4.jar | Bin 0 -> 185140 bytes nbproject/project.properties | 2 ++ src/mining/AbstractRequester.java | 72 ++++++++++++++++++++++++++++++++++++++ src/mining/BearerRequester.java | 72 ++++++++++++++++++++++++++++++++++++++ src/mining/OAuthRequester.java | 35 ++++++++++++++++++ src/mining/Requester.java | 23 ++++++++++++ 6 files changed, 204 insertions(+) create mode 100644 lib/commons-io-2.4.jar create mode 100644 src/mining/AbstractRequester.java create mode 100644 src/mining/BearerRequester.java create mode 100644 src/mining/OAuthRequester.java create mode 100644 src/mining/Requester.java diff --git a/lib/commons-io-2.4.jar b/lib/commons-io-2.4.jar new file mode 100644 index 0000000..90035a4 Binary files /dev/null and b/lib/commons-io-2.4.jar differ diff --git a/nbproject/project.properties b/nbproject/project.properties index 57f05f8..e5aabb7 100644 --- a/nbproject/project.properties +++ b/nbproject/project.properties @@ -31,6 +31,7 @@ file.reference.collections-generic-4.01.jar=lib/collections-generic-4.01.jar file.reference.colt-1.2.0.jar=lib/colt-1.2.0.jar file.reference.commons-codec-1.7.jar=lib/commons-codec-1.7.jar file.reference.commons-httpclient-3.1_1.jar=lib/commons-httpclient-3.1_1.jar +file.reference.commons-io-2.4.jar=lib/commons-io-2.4.jar file.reference.commons-lang-2.6.jar=lib/commons-lang-2.6.jar file.reference.commons-logging-1.1.1.jar=lib/commons-logging-1.1.1.jar file.reference.concurrent-1.3.4.jar=lib/concurrent-1.3.4.jar @@ -66,6 +67,7 @@ javac.classpath=\ ${file.reference.colt-1.2.0.jar}:\ ${file.reference.commons-codec-1.7.jar}:\ ${file.reference.commons-httpclient-3.1_1.jar}:\ + ${file.reference.commons-io-2.4.jar}:\ ${file.reference.commons-lang-2.6.jar}:\ ${file.reference.commons-logging-1.1.1.jar}:\ ${file.reference.concurrent-1.3.4.jar}:\ diff --git a/src/mining/AbstractRequester.java b/src/mining/AbstractRequester.java new file mode 100644 index 0000000..a42eb80 --- /dev/null +++ b/src/mining/AbstractRequester.java @@ -0,0 +1,72 @@ +package mining; + +import java.io.IOException; +import java.io.StringWriter; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import org.apache.commons.io.Charsets; +import org.apache.commons.io.IOUtils; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Performs an API Request. + * + * @author Peter Wu + */ +public abstract class AbstractRequester implements Requester { + + @Override + public JSONObject getJSON(String resource) throws IOException { + HttpURLConnection conn = open(buildUrl(resource)); + preconnect(conn); + if (conn.getResponseCode() != 200) { + // TODO: print more helpful details + throw new IOException("Unexpected response code"); + } + return getResponseAsJson(conn); + } + + protected final URL buildUrl(String resource) throws IOException { + String spec = "https://api.twitter.com/1.1/"; + // TODO: detect API version? For example, drop 1.1 for oauth2/token + spec += resource; + return new URL(spec); + } + + /** + * Opens a connection to the URL. + */ + protected final HttpURLConnection open(URL url) throws IOException { + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + // set default param: fail if no response within 5 seconds + conn.setReadTimeout(5000); + return conn; + } + + /** + * Reads the response body from a connection (in JSON format). + */ + protected final JSONObject getResponseAsJson(URLConnection conn) + throws IOException { + StringWriter writer = new StringWriter(); + IOUtils.copy(conn.getInputStream(), writer, Charsets.UTF_8); + try { + return new JSONObject(writer.toString()); + } catch (JSONException ex) { + // treat JSON errors as if an I/O error occurred + throw new IOException(ex); + } + } + + /** + * Prepare the request before it gets send. + * + * @param conn A connection for the request. + * @throws java.io.IOException on failing to prepare the request. + */ + protected abstract void preconnect(URLConnection conn) + throws IOException; +} diff --git a/src/mining/BearerRequester.java b/src/mining/BearerRequester.java new file mode 100644 index 0000000..94c5314 --- /dev/null +++ b/src/mining/BearerRequester.java @@ -0,0 +1,72 @@ +package mining; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.io.Charsets; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * An API requester used for application-only requests. Not all requests are + * allowed, see https://dev.twitter.com/docs/auth/application-only-auth for + * details. + * + * @author Peter Wu + */ +public class BearerRequester extends AbstractRequester { + + private final String access_token; + + /** + * + * @param access_token The bearer token that authenticates a request. + */ + public BearerRequester(String access_token) { + this.access_token = access_token; + } + + public BearerRequester(String consumerKey, String consumerSecret) + throws IOException { + String creds = encodeToken(consumerKey, consumerSecret); + String postData = "grant_type=client_credentials"; + URL url = buildUrl("oauth2/token"); + HttpURLConnection conn = open(url); + conn.setRequestMethod("POST"); + // set request headers + conn.addRequestProperty("Authorization", "Basic " + creds); + conn.addRequestProperty("Content-Type", + "application/x-www-form-urlencoded; charset=UTF-8"); + conn.setFixedLengthStreamingMode(postData.length()); + // connect and send request + conn.getOutputStream().write(postData.getBytes(Charsets.UTF_8)); + + try { + JSONObject resp = getResponseAsJson(conn); + // TODO: parse resp.errors + if (!resp.getString("token_type").equals("bearer")) { + throw new IOException("Expected bearer token type"); + } + access_token = resp.getString("access_token"); + } catch (JSONException ex) { + // treat JSON errors as if an I/O error occurred + throw new IOException(ex); + } + } + + @Override + protected void preconnect(URLConnection conn) throws IOException { + // requests do not have to be signed, instead rely on the Bearer token + conn.addRequestProperty("Authorization", "Bearer " + access_token); + } + + private String encodeToken(String consumerKey, String consumerSecret) { + StringBuilder sb = new StringBuilder(); + sb.append(consumerKey); + sb.append(":"); + sb.append(consumerSecret); + return Base64.encodeBase64String(sb.toString().getBytes(Charsets.UTF_8)); + } +} diff --git a/src/mining/OAuthRequester.java b/src/mining/OAuthRequester.java new file mode 100644 index 0000000..e72360e --- /dev/null +++ b/src/mining/OAuthRequester.java @@ -0,0 +1,35 @@ +package mining; + +import java.io.IOException; +import java.net.URLConnection; +import oauth.signpost.OAuthConsumer; +import oauth.signpost.basic.DefaultOAuthConsumer; +import oauth.signpost.exception.OAuthException; + +/** + * An API requester that uses OAuth to sign its requests. + * + * @author Peter Wu + */ +public class OAuthRequester extends AbstractRequester { + + /** + * Instance that signs signs HTTP requests with an OAuth token and secret. + */ + private final OAuthConsumer consumer; + + public OAuthRequester(String consumerKey, String consumerSecret) { + // create a new application-specific OAuth consumer + consumer = new DefaultOAuthConsumer(consumerKey, consumerSecret); + // TODO: access tokens? + } + + @Override + protected void preconnect(URLConnection conn) throws IOException { + try { + consumer.sign(conn); + } catch (OAuthException ex) { + throw new IOException(ex); + } + } +} diff --git a/src/mining/Requester.java b/src/mining/Requester.java new file mode 100644 index 0000000..931b0ac --- /dev/null +++ b/src/mining/Requester.java @@ -0,0 +1,23 @@ +package mining; + +import java.io.IOException; +import org.json.JSONObject; + +/** + * Performs an API request. + * + * @author Peter Wu + */ +public interface Requester { + + /** + * Performs an API request for a resource, for example + * "statuses/mentions_timeline" (note that there is no version or leading + * slash). + * + * @param resource The REST resource. + * @return A JSON object resulting from the request. + * @throws java.io.IOException on error fetching the resource. + */ + public JSONObject getJSON(String resource) throws IOException; +} -- cgit v1.2.1