package io; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.StringWriter; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; import java.util.logging.Logger; 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 { private static final String API_URL = "https://api.twitter.com/"; @Override public JSONObject getJSON(String resource) throws IOException { return getJSON(resource, true); } @Override public JSONObject getJSONRelax(String resource) throws IOException { return getJSON(resource, false); } private JSONObject getJSON(String resource, boolean checkStatusCode) throws IOException { HttpURLConnection conn = open(buildUrl(resource)); try { preconnect(conn); JSONObject resp = getResponseAsJson(conn); /* print response to stderr for debugging */ if (resp.has("errors")) { try { String errors = resp.get("errors").toString(); getLogger().info("Request failed: " + errors); } catch (JSONException ex) { } } // TODO: what if there is an internal server error? Technically we // should always treat that as "don't know what the server thinks". if (checkStatusCode && conn.getResponseCode() != 200) { // TODO: print more helpful details throw new IOException("Unexpected response code"); } return resp; } finally { conn.disconnect(); } } protected final URL buildUrl(String resource) throws IOException { String spec = API_URL; String[] split = resource.split("\\?", 2); boolean isPreV1; isPreV1 = resource.startsWith("oauth/") || resource.startsWith("oauth2/"); if (!isPreV1 && !resource.startsWith("1")) { // manual inspection shows that at least oauth/ and oauth2/ do not // have a version prefixed. Do not add version if already given // (e.g. "1/account/rate_limit_status"). spec += "1.1/"; } // path part spec += split[0]; if (!isPreV1) { // not sure about the history, but oauth{,2}/ don't have this. spec += ".json"; } // append parameters after path if (split.length >= 2) { spec += "?" + split[1]; } return new URL(spec); } /** * Opens a connection to the URL. * * @param url The URL to open a connection to. * @return a connection that can be used for sending requests. * @throws java.io.IOException on failure to open a connection. */ protected final HttpURLConnection open(URL url) throws IOException { getLogger().fine("Opening: " + url); 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). * * @param conn An open connection to which a request was already made. * @return A JSON object as parsed from the response. * @throws java.io.IOException if the response cannot be retrieved or if the * response does not contain well-formed JSON. */ protected final JSONObject getResponseAsJson(HttpURLConnection conn) throws IOException { StringWriter writer = new StringWriter(); InputStream is; try { is = conn.getInputStream(); } catch (FileNotFoundException ex) { /* 404 (FileNotFoundException) */ is = conn.getErrorStream(); } // this could happen if the URL was severly malformed if (is == null) { throw new IOException("Failed to fetch response"); } IOUtils.copy(is, 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; private Logger getLogger() { return Logger.getLogger(getClass().getName()); } }