summaryrefslogtreecommitdiff
path: root/src/io/AbstractRequester.java
blob: 239e75d1fbbfa8a65347d75d41f36142ae1f5926 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
package io;

import com.google.gson.JsonElement;
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 {
        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) {
                // 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());
    }
}