package io; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; 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.Level; import java.util.logging.Logger; import org.apache.commons.io.Charsets; import org.apache.commons.io.IOUtils; /** * 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 Response getJSON(String resource) throws IOException, RateLimitException { HttpURLConnection conn = open(buildUrl(resource)); try { preconnect(conn); JsonElement resp = getResponseAsJson(conn); if (resp.isJsonObject() && resp.getAsJsonObject().has("errors")) { /* print response to stderr for debugging */ String errors = resp.getAsJsonObject().get("errors").toString(); getLogger().log(Level.INFO, "Request failed: {0}", errors); } // TODO: what if there is an internal server error? Technically we // should always treat that as "don't know what the server thinks". if (conn.getResponseCode() != 200) { if (conn.getResponseCode() == 429) { //TODO: wait for real time and not 15 minutes. throw new RateLimitException(Integer.parseInt(conn.getHeaderField("X-Rate-Limit-Reset"))); } // TODO: print more helpful details throw new IOException("Unexpected response code"); } int rateLimit = Integer.parseInt(conn.getHeaderField("X-Rate-Limit-Limit")); int rateLimitRemaining = Integer.parseInt(conn.getHeaderField("X-Rate-Limit-Remaining")); int rateLimitReset = Integer.parseInt(conn.getHeaderField("X-Rate-Limit-Reset")); return new Response(resp, rateLimit, rateLimitRemaining, rateLimitReset); } 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().log(Level.FINE, "Opening: {0}", 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 JsonElement 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); JsonParser parser = new JsonParser(); return parser.parse(writer.toString()); } /** * 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()); } }