package org.schtief.twitter; import java.io.BufferedReader; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.Reader; import java.io.UnsupportedEncodingException; import java.lang.reflect.Method; import java.net.HttpURLConnection; import java.net.SocketTimeoutException; import java.net.URL; import java.net.URLConnection; import java.net.URLEncoder; import java.util.Map; import java.util.Map.Entry; import org.schtief.util.json.JSONException; import org.schtief.util.json.JSONObject; /** * A simple http client that uses the built in URLConnection class. * * @author Daniel Winterstein * */ public class URLConnectionHttpClient implements Twitter.IHttpClient { private final String name; private final String password; /** * true if we are in the middle of a retry attempt. false normally */ private boolean retryingFlag; /** * If true, will wait 1 second and make a second when presented with a server error. */ private boolean retryOnError; public URLConnectionHttpClient(String name, String password) { this.name = name; this.password = password; assert (name!=null && password != null) || (name==null && password==null); } public URLConnectionHttpClient() { this(null,null); } public boolean canAuthenticate() { return name != null && password != null; } private static final int timeOutMilliSecs = 10 * 1000; public String getPage(String uri, Map<String, String> vars, boolean authenticate) throws TwitterException { assert uri != null; if (vars != null && vars.size() != 0) { uri += "?"; for (Entry<String, String> e : vars.entrySet()) { if (e.getValue() == null) continue; uri += encode(e.getKey()) + "=" + encode(e.getValue()) + "&"; } } try { // Setup a connection final HttpURLConnection connection = (HttpURLConnection) new URL(uri).openConnection(); // Authenticate if (authenticate) { setBasicAuthentication(connection, name, password); } // To keep the search API happy - which wants either a referrer or a user agent connection.setRequestProperty("User-Agent", "JTwitter/"+Twitter.version); connection.setDoInput(true); connection.setReadTimeout(timeOutMilliSecs); // Open a connection processError(connection); final InputStream inStream = connection.getInputStream(); // Read in the web page String page = toString(inStream); // Done return page; } catch (IOException e) { throw new TwitterException(e); } catch (TwitterException.E50X e) { if ( ! retryOnError || retryingFlag) throw e; try { retryingFlag = true; Thread.sleep(1000); return getPage(uri, vars, authenticate); } catch (InterruptedException ex) { throw new TwitterException(ex); } finally { retryingFlag = false; } } } /** * Throw an exception if the connection failed * @param connection */ void processError(HttpURLConnection connection) { try { int code = connection.getResponseCode(); if (code==200) return; URL url = connection.getURL(); String error = connection.getResponseMessage(); if (code==403) { throw new TwitterException.E403(error+" "+url+" ("+(name==null?"anonymous":name)+")"); } if (code==404) { throw new TwitterException.E404(error+" "+url); } if (code >= 500 && code<600) { throw new TwitterException.E50X(error+" "+url); } boolean rateLimitExceeded = error.contains("Rate limit exceeded"); if (rateLimitExceeded) { throw new TwitterException.RateLimit(error); } // Rate limiter can sometimes cause a 400 Bad Request if (code==400) { String json = getPage("http://twitter.com/account/rate_limit_status.json", null, true); try { JSONObject obj = new JSONObject(json); int hits = obj.getInt("remaining_hits"); if (hits<1) throw new TwitterException.RateLimit(error); } catch (JSONException e) { // oh well } } // just report it as a vanilla exception throw new TwitterException(code + " " + error+" "+url); } catch (SocketTimeoutException e) { URL url = connection.getURL(); throw new TwitterException.Timeout(timeOutMilliSecs+"milli-secs for "+url); } catch (IOException e) { throw new TwitterException(e); } } private String getErrorStream(HttpURLConnection connection) { try { return toString(connection.getErrorStream()); } catch (NullPointerException e) { return null; } } /** * Set a header for basic authentication login. */ private void setBasicAuthentication(URLConnection connection, String name, String password) { assert name != null && password != null; String token = name + ":" + password; String encoding = Base64Encoder.encode(token); connection.setRequestProperty("Authorization", "Basic " + encoding); } public String post(String uri, Map<String, String> vars, boolean authenticate) throws TwitterException { HttpURLConnection connection = null; try { connection = (HttpURLConnection) new URL(uri).openConnection(); connection.setRequestMethod("POST"); connection.setDoOutput(true); connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); connection.setReadTimeout(timeOutMilliSecs); if (authenticate) { setBasicAuthentication(connection, name, password); } StringBuilder encodedData = new StringBuilder(); if (vars != null) { for (String key : vars.keySet()) { String val = encode(vars.get(key)); encodedData.append(encode(key)); encodedData.append('='); encodedData.append(val); encodedData.append('&'); } } connection.setRequestProperty("Content-Length", "" + encodedData.length()); OutputStream os = connection.getOutputStream(); os.write(encodedData.toString().getBytes()); close(os); // Get the response processError(connection); String response = toString(connection.getInputStream()); return response; } catch (IOException e) { throw new TwitterException(e); } catch (TwitterException.E50X e) { if ( ! retryOnError || retryingFlag) throw e; try { Thread.sleep(1000); retryingFlag = true; return post(uri, vars, authenticate); } catch (InterruptedException ex) { throw new TwitterException(ex); } finally { retryingFlag = false; } } } /** * False by default. Setting this to true switches on a robustness * workaround: when presented with a 50X server error, the * system will wait 1 second and make a second attempt. */ public void setRetryOnError(boolean retryOnError) { this.retryOnError = retryOnError; } private static String encode(Object x) { return URLEncoder.encode(String.valueOf(x)); } /** * Use a bufferred reader (preferably UTF-8) to extract the contents of * the given stream. A convenience method for {@link #toString(Reader)}. */ private static String toString(InputStream inputStream) { InputStreamReader reader; try { reader = new InputStreamReader(inputStream, "UTF-8"); } catch (UnsupportedEncodingException e) { reader = new InputStreamReader(inputStream); } return toString(reader); } /** * Use a buffered reader to extract the contents of the given reader. * * @param reader * @return The contents of this reader. */ private static String toString(Reader reader) throws RuntimeException { try { // Buffer if not already buffered reader = reader instanceof BufferedReader ? (BufferedReader) reader : new BufferedReader(reader); StringBuilder output = new StringBuilder(); while (true) { int c = reader.read(); if (c == -1) break; output.append((char) c); } return output.toString(); } catch (IOException ex) { throw new RuntimeException(ex); } finally { close(reader); } } /** * Close a reader/writer/stream, ignoring any exceptions that result. Also * flushes if there is a flush() method. */ private static void close(Closeable input) { if (input == null) return; // Flush (annoying that this is not part of Closeable) try { Method m = input.getClass().getMethod("flush"); m.invoke(input); } catch (Exception e) { // Ignore } // Close try { input.close(); } catch (IOException e) { // Ignore } } }