summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorPeter Wu <peter@lekensteyn.nl>2014-04-23 19:51:17 +0200
committerPeter Wu <peter@lekensteyn.nl>2014-04-23 19:52:24 +0200
commit532efef6350682fa3eb136ee08d106791deb360a (patch)
tree195d605ebb44df7e61c11d225dea434044f4403a /src
parent86827fff1d1fba26db4f80e93e27b6b7705a9661 (diff)
downloadTwitterDataAnalytics-532efef6350682fa3eb136ee08d106791deb360a.tar.gz
[WIP] API requester abstraction
Authentication, etc. is shuffled into Requester. Implementators (currently OAuth and Bearer) should prepare proper requests.
Diffstat (limited to 'src')
-rw-r--r--src/mining/AbstractRequester.java72
-rw-r--r--src/mining/BearerRequester.java72
-rw-r--r--src/mining/OAuthRequester.java35
-rw-r--r--src/mining/Requester.java23
4 files changed, 202 insertions, 0 deletions
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;
+}