Java tutorial
/* * Copyright (C) 2009 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ /** * Talking RSS Reader. * * @author sdoyon@google.com (Stephane Doyon) */ package com.googlecode.talkingrssreader.talkingrss; import android.util.Config; import android.util.Log; import android.os.SystemClock; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringWriter; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.net.SocketException; import java.net.URLEncoder; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.HttpClient; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.message.BasicNameValuePair; import org.apache.http.NameValuePair; import org.apache.http.StatusLine; import com.googlecode.talkingrssreader.talkingrss.ReaderExceptions.UnexpectedException; import com.googlecode.talkingrssreader.talkingrss.ReaderExceptions.ReaderException; import com.googlecode.talkingrssreader.talkingrss.ReaderExceptions.NetworkException; import com.googlecode.talkingrssreader.talkingrss.ReaderExceptions.ProtocolException; import com.googlecode.talkingrssreader.talkingrss.ReaderExceptions.HttpException; import com.googlecode.talkingrssreader.talkingrss.ReaderExceptions.HttpForbiddenException; /** ReaderHttp: Connecting to the unofficial Google Reader API over HTTP. */ public class ReaderHttp { private static final String TAG = "talkingrss-http"; private static final String AGENT_STRING = "Talkingrss-0.1"; private static final String AUTHORIZATION_HEADER_NAME = "Authorization"; private static final String AUTHORIZATION_HEADER_VALUE_PREFIX = "GoogleLogin auth="; private static final String GOOGLE_LOGIN_URL = "https://www.google.com/accounts/ClientLogin"; private static final String READER_BASE_URL = "http://www.google.com/reader/"; private static final String API_URL = READER_BASE_URL + "api/0/"; private static final String TOKEN_URL = API_URL + "token"; private static final String SUBSCRIPTION_URL = API_URL + "subscription/edit"; private static final String TAG_URL = API_URL + "edit-tag"; private static final String DISABLE_TAG_URL = API_URL + "disable-tag"; private static final String ATOM_URL = READER_BASE_URL + "atom/"; public static final String READING_LIST_STATE = "user/-/state/com.google/reading-list"; public static final String READ_STATE = "user/-/state/com.google/read"; public static final String KEEP_UNREAD_STATE = "user/-/state/com.google/keep-unread"; public static final String FRESH_STATE = "user/-/state/com.google/fresh"; public static final String STARRED_STATE = "user/-/state/com.google/starred"; public static final String BROADCAST_STATE = "user/-/state/com.google/broadcast"; private static final String USER_LABEL = "user/-/label/"; public static final String FEED_PREFIX = "feed/"; public static final String TAG_LIST = "tag/list"; public static final String SUBSCRIPTION_LIST = "subscription/list"; public static final String UNREAD_COUNT_LIST = "unread-count"; public static final int MAX_NUM_ITEMS_TO_TAG = 250; private static final boolean verbose = false; private String clientParam; private String authToken; // ClientLogin. private String apiToken; ThreadLocal<HttpClient> httpClientPerThread = new ThreadLocal<HttpClient>(); public ReaderHttp() { try { clientParam = "client=" + URLEncoder.encode(AGENT_STRING, "UTF-8"); } catch (UnsupportedEncodingException e) { throw new UnexpectedException(e); } } public void setAuthToken(String authToken) { this.authToken = authToken; } private HttpClient getHttpClient() { HttpClient httpClient = httpClientPerThread.get(); if (httpClient == null) { httpClient = new DefaultHttpClient(); httpClientPerThread.set(httpClient); } return httpClient; } public static String urlEncode(String s) { try { return URLEncoder.encode(s, "UTF-8"); } catch (UnsupportedEncodingException e) { throw new UnexpectedException(e); } } // Encodes HTTP query parameters. public static String EncodeQueryParams(ArrayList<NameValuePair> params) { String query_string = ""; // There's got to be a simpler way to do this... for (NameValuePair n : params) { try { query_string += (query_string.length() == 0 ? "" : "&") + URLEncoder.encode(n.getName(), "UTF-8") + "=" + URLEncoder.encode(n.getValue(), "UTF-8"); } catch (UnsupportedEncodingException e) { throw new UnexpectedException(e); } } return query_string; } // Performs an HTTP request (GET or POST) and returns an InputStream // to the reply. private InputStream doHttpRequest(HttpUriRequest request) throws ReaderException { try { HttpResponse response = getHttpClient().execute(request); StatusLine status = response.getStatusLine(); if (status.getStatusCode() != HttpStatus.SC_OK) { request.abort(); throw HttpException.httpException(status.getStatusCode(), status.getReasonPhrase()); } HttpEntity reply = response.getEntity(); if (reply == null) { request.abort(); throw new ProtocolException("null response entity"); } return reply.getContent(); // an InputStream } catch (IOException e) { request.abort(); throw new NetworkException(e); } } // Prepares and performs a POST request. private InputStream doPost(String url, ArrayList<NameValuePair> params, boolean addAuthHeaderAndApiToken) throws ReaderException { if (verbose) Log.d(TAG, url); HttpPost http_post = new HttpPost(url); if (addAuthHeaderAndApiToken) { http_post.addHeader(AUTHORIZATION_HEADER_NAME, AUTHORIZATION_HEADER_VALUE_PREFIX + authToken); if (params != null) { params.add(new BasicNameValuePair("client", AGENT_STRING)); params.add(new BasicNameValuePair("T", apiToken)); } } try { UrlEncodedFormEntity post_entity = new UrlEncodedFormEntity(params); /* if (verbose) { try { // TODO: following does not actually work on the phone. post_entity.writeTo(System.out); System.out.println(); System.out.println(); } catch(IOException e) { e.printStackTrace(); } } */ http_post.setEntity(post_entity); } catch (UnsupportedEncodingException e) { throw new UnexpectedException(e); } return doHttpRequest(http_post); } // Prepares and performs a GET request. private InputStream doGet(String url, ArrayList<NameValuePair> params) throws ReaderException { String query_string = ""; if (params != null) query_string = EncodeQueryParams(params); query_string += (query_string.length() == 0 ? "" : "&") + clientParam; url += "?" + query_string; if (verbose) Log.d(TAG, url); HttpGet http_get = new HttpGet(url); http_get.addHeader(AUTHORIZATION_HEADER_NAME, AUTHORIZATION_HEADER_VALUE_PREFIX + authToken); return doHttpRequest(http_get); } // Reads an InputStream until the end and returns the content in a String. private static String readAll(InputStream stream) throws NetworkException { BufferedReader reader = new BufferedReader(new InputStreamReader(stream), 16 * 1024); StringWriter sw = new StringWriter(); char[] buf = new char[32 * 1024]; try { while (true) { int len = reader.read(buf); if (len == -1) break; sw.write(buf, 0, len); } } catch (IOException e) { throw new NetworkException(e); } finally { try { reader.close(); } catch (IOException e) { } } return sw.toString(); } // ClientLogin. public String login(String username, String password) throws ReaderException { if (Config.LOGD) Log.d(TAG, "Logging in"); long startTime = SystemClock.uptimeMillis(); ArrayList<NameValuePair> params = new ArrayList<NameValuePair>(); params.add(new BasicNameValuePair("accountType", "HOSTED_OR_GOOGLE")); params.add(new BasicNameValuePair("Email", username)); params.add(new BasicNameValuePair("Passwd", password)); params.add(new BasicNameValuePair("service", "reader")); params.add(new BasicNameValuePair("source", AGENT_STRING)); String reply; try { reply = readAll(doPost(GOOGLE_LOGIN_URL, params, false)); } catch (HttpForbiddenException e) { return null; } long now = SystemClock.uptimeMillis(); if (Config.LOGD) Log.d(TAG, String.format("Login request took %dms", now - startTime)); int index = reply.indexOf("Auth="); if (index > -1) { int end = reply.indexOf('\n', index); if (end > index + 5) { String authToken = reply.substring(index + 5, end); return authToken; } } throw new ProtocolException("Failed to parse login reply"); } public String getArticlesByTag(String tag, String[] exclude, int howMany, String continuation) throws ReaderException { // Escaping is unclear. Feed in the form http://blabla.com/bla // strangely mustn't be escaped, else they won't be // recognized. Spaces in user labels need %20 escaping, unclear // about other chars. if (tag.startsWith("user/") && tag.contains("/label/")) { int index = tag.lastIndexOf("/"); if (index > 0 && index < tag.length() - 1) { String tip = tag.substring(index + 1); tip = urlEncode(tip).replace("+", "%20"); tag = tag.substring(0, index + 1) + tip; } } String url = ATOM_URL + tag; ArrayList<NameValuePair> params = new ArrayList<NameValuePair>(); params.add(new BasicNameValuePair("n", String.valueOf(howMany))); if (continuation != null) params.add(new BasicNameValuePair("c", continuation)); if (exclude != null) { for (String x : exclude) { params.add(new BasicNameValuePair("xt", x)); } } long startTime = SystemClock.uptimeMillis(); InputStream is = doGet(url, params); long readTime = SystemClock.uptimeMillis(); String out = readAll(is); long now = SystemClock.uptimeMillis(); if (Config.LOGD) Log.d(TAG, String.format("Article fetch: request took %dms, transferred %dbytes in %dms", readTime - startTime, out.length(), now - readTime)); return out; } /* public static String userLabel(String label) { return USER_LABEL + urlEncode(label); } */ public String getList(String list) throws ReaderException { String url = API_URL + list; ArrayList<NameValuePair> params = new ArrayList<NameValuePair>(); params.add(new BasicNameValuePair("output", "xml")); //all=true long startTime = SystemClock.uptimeMillis(); InputStream is = doGet(url, params); long readTime = SystemClock.uptimeMillis(); String out = readAll(is); long now = SystemClock.uptimeMillis(); if (Config.LOGD) Log.d(TAG, String.format("list fetch: request took %dms, transferred %dbytes in %dms", readTime - startTime, out.length(), now - readTime)); return out; } // Obtain the short-lived token used to authenticate API operations. private void fetchApiToken() throws ReaderException { if (Config.LOGD) Log.d(TAG, "Fetching API token"); apiToken = readAll(doGet(TOKEN_URL, null)); } // Performs a POST request for an API operation, first obtaining an // API token if we don't have one, and retrying the operation with a // fresh token should the current token expire. private void doApiPost(String url, ArrayList<NameValuePair> params) throws ReaderException { long startTime = SystemClock.uptimeMillis(); boolean do_retry = true; if (apiToken == null) { fetchApiToken(); do_retry = false; } try { checkApiOk(doPost(url, params, true)); } catch (HttpException e) { if (do_retry && (e.code == HttpStatus.SC_BAD_REQUEST || e.code == HttpStatus.SC_UNAUTHORIZED)) { // Get a fresh token fetchApiToken(); checkApiOk(doPost(url, params, true)); } else { throw e; } } long now = SystemClock.uptimeMillis(); if (Config.LOGD) Log.d(TAG, String.format("API request took %dms", now - startTime)); } // Validates the "OK" reply for a successful API operation. private static void checkApiOk(InputStream stream) throws NetworkException, ProtocolException { String reply = readAll(stream).trim(); if (!reply.equals("OK")) throw new ProtocolException("API failure"); } public void subscribeFeed(String feedId, boolean add) throws ReaderException { String action = add ? "subscribe" : "unsubscribe"; if (Config.LOGD) Log.d(TAG, String.format("%s feed: %s", action, feedId)); if (!feedId.startsWith(FEED_PREFIX)) throw new IllegalArgumentException(); ArrayList<NameValuePair> params = new ArrayList<NameValuePair>(); params.add(new BasicNameValuePair("s", feedId)); params.add(new BasicNameValuePair("ac", action)); doApiPost(SUBSCRIPTION_URL, params); } public void tagItems(ArrayList<String> itemIds, String[] addTags, String[] removeTags) throws ReaderException { if (Config.LOGD) Log.d(TAG, "Tagging " + String.valueOf(itemIds.size()) + " items"); ArrayList<NameValuePair> params = new ArrayList<NameValuePair>(); for (String item : itemIds) params.add(new BasicNameValuePair("i", item)); if (addTags != null) { for (String addTag : addTags) { params.add(new BasicNameValuePair("a", addTag)); } } if (removeTags != null) { for (String removeTag : removeTags) { params.add(new BasicNameValuePair("r", removeTag)); } } params.add(new BasicNameValuePair("ac", "edit")); doApiPost(TAG_URL, params); } public void tagFeed(String feedId, String tag, boolean add) throws ReaderException { if (Config.LOGD) Log.d(TAG, String.format("%s feed %s as %s", (add ? "tagging" : "untagging"), feedId, tag)); ArrayList<NameValuePair> params = new ArrayList<NameValuePair>(); params.add(new BasicNameValuePair("s", feedId)); params.add(new BasicNameValuePair(add ? "a" : "r", tag)); params.add(new BasicNameValuePair("ac", "edit")); doApiPost(SUBSCRIPTION_URL, params); } public void disableTag(String tag) throws ReaderException { if (Config.LOGD) Log.d(TAG, "Disabling tag: " + tag); ArrayList<NameValuePair> params = new ArrayList<NameValuePair>(); params.add(new BasicNameValuePair("s", tag)); params.add(new BasicNameValuePair("ac", "disable-tags")); doApiPost(DISABLE_TAG_URL, params); } }