org.thomnichols.android.gmarks.BookmarksQueryService.java Source code

Java tutorial

Introduction

Here is the source code for org.thomnichols.android.gmarks.BookmarksQueryService.java

Source

/* This file is part of GMarks. Copyright 2010, 2011 Thom Nichols
 *
 * GMarks is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * GMarks is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with GMarks.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.thomnichols.android.gmarks;

import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;

import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.CookieStore;
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.client.protocol.ClientContext;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.cookie.Cookie;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import org.thomnichols.android.gmarks.thirdparty.IOUtils;

import android.net.Uri;
import android.net.http.AndroidHttpClient;
import android.util.Log;

/**
 * @author tnichols
 *
 */
public class BookmarksQueryService {

    public synchronized static BookmarksQueryService getInstance() {
        if (instance == null)
            instance = new BookmarksQueryService(null);
        return instance;
    }

    public class AuthException extends IOException {
        private static final long serialVersionUID = 1L;

        public AuthException() {
            super();
        }

        public AuthException(String msg) {
            super(msg);
        }

        public AuthException(Throwable cause) {
            super(cause);
        }

        public AuthException(String msg, Throwable cause) {
            super(msg, cause);
        }
    }

    public class NotFoundException extends IOException {
        private static final long serialVersionUID = 1L;

        public NotFoundException() {
            super();
        }

        public NotFoundException(String msg) {
            super(msg);
        }

        public NotFoundException(Throwable cause) {
            super(cause);
        }

        public NotFoundException(String msg, Throwable cause) {
            super(msg, cause);
        }
    }

    private static BookmarksQueryService instance = null;

    //   protected DefaultHttpClient http;
    protected AndroidHttpClient http;
    protected HttpContext ctx;
    //   protected String USER_AGENT = "";
    protected CookieStore cookieStore;
    protected String TAG = "GMARKS REMOTE SVC";
    protected boolean authInitialized = false;
    protected String xtParam = null;
    protected String mainThreadId = null;

    private BookmarksQueryService(String userAgent) {
        //      java.util.logging.Logger.getLogger("httpclient.wire.header").setLevel(java.util.logging.Level.FINEST);
        //      java.util.logging.Logger.getLogger("httpclient.wire.content").setLevel(java.util.logging.Level.FINEST);
        ctx = new BasicHttpContext();
        cookieStore = new BasicCookieStore();
        ctx.setAttribute(ClientContext.COOKIE_STORE, cookieStore);
        String defaultUA = "Mozilla/5.0 (Linux; U; Android 2.1; en-us) AppleWebKit/522+ (KHTML, like Gecko) Safari/419.3";
        //      http = new DefaultHttpClient();
        http = AndroidHttpClient.newInstance(userAgent != null ? userAgent : defaultUA);
    }

    public void setAuthCookies(List<Cookie> cookies) {
        this.cookieStore.clear();
        for (Cookie c : cookies)
            this.cookieStore.addCookie(c);
        // TODO if cookies are expired, don't set them & notify caller they should be refreshed.
        this.authInitialized = true;
    }

    public boolean isAuthInitialized() {
        return authInitialized;
    }

    public void clearAuthCookies() {
        this.cookieStore.clear();
        this.authInitialized = false;
    }

    public void login(String user, String passwd) {
        try {
            List<NameValuePair> queryParams = new ArrayList<NameValuePair>();
            queryParams.add(new BasicNameValuePair("service", "bookmarks"));
            queryParams.add(new BasicNameValuePair("passive", "true"));
            queryParams.add(new BasicNameValuePair("nui", "1"));
            queryParams.add(new BasicNameValuePair("continue", "https://www.google.com/bookmarks/l"));
            queryParams.add(new BasicNameValuePair("followup", "https://www.google.com/bookmarks/l"));
            HttpGet get = new HttpGet(
                    "https://www.google.com/accounts/ServiceLogin?" + URLEncodedUtils.format(queryParams, "UTF-8"));
            HttpResponse resp = http.execute(get, this.ctx);
            // this just gets the cookie but I can ignore it...

            if (resp.getStatusLine().getStatusCode() != 200)
                throw new RuntimeException(
                        "Invalid status code for ServiceLogin " + resp.getStatusLine().getStatusCode());
            resp.getEntity().consumeContent();

            String galx = null;
            for (Cookie c : cookieStore.getCookies())
                if (c.getName().equals("GALX"))
                    galx = c.getValue();

            if (galx == null)
                throw new RuntimeException("GALX cookie not found!");

            HttpPost loginMethod = new HttpPost("https://www.google.com/accounts/ServiceLoginAuth");
            // post parameters:
            List<NameValuePair> nvps = new ArrayList<NameValuePair>();
            nvps.add(new BasicNameValuePair("Email", user));
            nvps.add(new BasicNameValuePair("Passwd", passwd));
            nvps.add(new BasicNameValuePair("PersistentCookie", "yes"));
            nvps.add(new BasicNameValuePair("GALX", galx));
            nvps.add(new BasicNameValuePair("continue", "https://www.google.com/bookmarks/l"));
            loginMethod.setEntity(new UrlEncodedFormEntity(nvps));
            resp = http.execute(loginMethod, this.ctx);

            if (resp.getStatusLine().getStatusCode() != 302)
                throw new RuntimeException(
                        "Unexpected status code for ServiceLoginAuth" + resp.getStatusLine().getStatusCode());
            resp.getEntity().consumeContent();

            Header checkCookieLocation = resp.getFirstHeader("Location");
            if (checkCookieLocation == null)
                throw new RuntimeException("Missing checkCookie redirect location!");

            // CheckCookie:
            get = new HttpGet(checkCookieLocation.getValue());
            resp = http.execute(get, this.ctx);

            if (resp.getStatusLine().getStatusCode() != 302)
                throw new RuntimeException(
                        "Unexpected status code for CheckCookie" + resp.getStatusLine().getStatusCode());
            resp.getEntity().consumeContent();

            this.authInitialized = true;
            Log.i(TAG, "Final redirect location: " + resp.getFirstHeader("Location").getValue());
            Log.i(TAG, "Logged in.");
        } catch (IOException ex) {
            Log.e(TAG, "Error during login", ex);
            throw new RuntimeException("IOException during login", ex);
        }
    }

    public boolean testAuth() {
        HttpGet get = new HttpGet("https://www.google.com/bookmarks/api/threadsearch?fo=Starred&g&q&start&nr=1");
        try {
            HttpResponse resp = http.execute(get, this.ctx);
            int statusCode = resp.getStatusLine().getStatusCode();
            Log.d(TAG, "testAuth return code: " + statusCode);
            return statusCode < 400;
        } catch (IOException ex) {
            Log.e(TAG, "Error while checking auth status", ex);
        }
        return false;
    }

    public Bookmark create(Bookmark b) throws IOException {
        final String createURL = "https://www.google.com/bookmarks/api/thread?op=Star" + "&xt="
                + URLEncoder.encode(getXtParam(), "UTF-8");

        //td {"results":[{"threadId":"BDQAAAAAQAA","elementId":0,"authorId":0,
        //                "title":"My Blog","timestamp":0,"formattedTimestamp":0,
        //                "url":"http://blog.thomnichols.org","signedUrl":"",
        //                "previewUrl":"","snippet":"___________","threadComments":[],
        //                "parentId":"BDQAAAAAQAA","labels":["mobile"]}]}
        JSONObject requestObj = new JSONObject();
        try {
            JSONObject bookmarkObj = new JSONObject();
            // TODO this is part of a bookmark but I've been ignoring it...
            bookmarkObj.put("threadId", this.mainThreadId);
            bookmarkObj.put("elementId", 0);
            bookmarkObj.put("title", b.getTitle());
            bookmarkObj.put("url", b.getUrl());
            bookmarkObj.put("snippet", b.getDescription());
            JSONArray labels = new JSONArray();
            for (String label : b.getLabels())
                labels.put(label);
            bookmarkObj.put("labels", labels);

            bookmarkObj.put("timestamp", 0);
            bookmarkObj.put("formattedTimestamp", 0);
            bookmarkObj.put("authorId", 0);
            bookmarkObj.put("signedUrl", "");
            bookmarkObj.put("previewUrl", "");
            bookmarkObj.put("threadComments", new JSONArray());
            // this is the same as threadId...  Do I need to know the value for this??
            bookmarkObj.put("parentId", this.mainThreadId);

            JSONArray resultArray = new JSONArray();
            resultArray.put(bookmarkObj);
            requestObj.put("results", resultArray);
        } catch (JSONException ex) {
            throw new IOException("Error creating request", ex);
        }

        return createOrUpdate(createURL, requestObj);
    }

    public Bookmark update(Bookmark b) throws IOException {
        String updateURL = "https://www.google.com/bookmarks/api/thread?op=UpdateThreadElement" + "&xt="
                + URLEncoder.encode(getXtParam(), "UTF-8");

        JSONObject requestObj = new JSONObject();
        try {
            JSONObject bookmarkObj = new JSONObject();
            // TODO this is part of a bookmark but I've been ignoring it...
            bookmarkObj.put("threadId", b.getThreadId());
            bookmarkObj.put("elementId", b.getGoogleId());
            bookmarkObj.put("title", b.getTitle());
            bookmarkObj.put("url", b.getUrl());
            bookmarkObj.put("snippet", b.getDescription());
            JSONArray labels = new JSONArray();
            for (String label : b.getLabels())
                labels.put(label);
            bookmarkObj.put("labels", labels);

            // these are in the request but empty... maybe we can ignore them???
            //         "authorId":0,"timestamp":0,"formattedTimestamp":0,"signedUrl":"",
            //         "previewUrl":"","threadComments":[],"parentId":"",
            bookmarkObj.put("authorId", 0);
            bookmarkObj.put("timestamp", 0);
            bookmarkObj.put("formattedTimestamp", 0);
            bookmarkObj.put("signedUrl", "");
            bookmarkObj.put("previewUrl", "");
            bookmarkObj.put("threadComments", new JSONArray());
            bookmarkObj.put("parentId", "");

            JSONArray results = new JSONArray();
            results.put(bookmarkObj);
            requestObj.put("threadResults", results);

            // other unneeded params that are part of an update request:
            JSONArray emptyArray = new JSONArray();
            requestObj.put("threads", emptyArray);
            requestObj.put("threadQueries", emptyArray);
            requestObj.put("threadComments", emptyArray);

        } catch (JSONException ex) {
            throw new IOException("Error creating request", ex);
        }

        Bookmark newBookmark = createOrUpdate(updateURL, requestObj);
        if (b.get_id() != null)
            newBookmark.set_id(b.get_id());
        // TODO created date is not in response
        //      if ( b.getCreatedDate() != 0 ) newBookmark.setCreatedDate(b.getCreatedDate());
        return newBookmark;
    }

    public void delete(String googleId) throws AuthException, NotFoundException, IOException {
        Uri requestURI = Uri.parse("https://www.google.com/bookmarks/api/thread").buildUpon()
                .appendQueryParameter("xt", getXtParam()).appendQueryParameter("op", "DeleteItems").build();
        //      final String deleteURL = "https://www.google.com/bookmarks/api/thread?"
        //         + "xt=" + URLEncoder.encode( getXtParam(), "UTF-8" )  
        //         + "&op=DeleteItems";
        //  td   {"deleteAllBookmarks":false,"deleteAllThreads":false,"urls":[],"ids":["____"]}

        JSONObject requestObj = new JSONObject();
        try {
            requestObj.put("deleteAllBookmarks", false);
            requestObj.put("deleteAllThreads", false);
            requestObj.put("urls", new JSONArray());
            JSONArray elementIDs = new JSONArray();
            elementIDs.put(googleId);
            requestObj.put("ids", elementIDs);
        } catch (JSONException ex) {
            throw new IOException("JSON error while creating request");
        }

        String postString = "{\"deleteAllBookmarks\":false,\"deleteAllThreads\":false,\"urls\":[],\"ids\":[\""
                + googleId + "\"]}";

        List<NameValuePair> params = new ArrayList<NameValuePair>();
        //      params.add( new BasicNameValuePair("td", requestObj.toString()) );
        params.add(new BasicNameValuePair("td", postString));

        //      Log.v(TAG,"DELETE: " + requestObj.toString());
        Log.v(TAG, "DELETE: " + requestURI);
        Log.v(TAG, "DELETE: " + postString);

        HttpPost post = new HttpPost(requestURI.toString());
        //      HttpPost post = new HttpPost( deleteURL );      
        post.setEntity(new UrlEncodedFormEntity(params));
        HttpResponse resp = http.execute(post, this.ctx);

        int respCode = resp.getStatusLine().getStatusCode();
        if (respCode == 401)
            throw new AuthException();
        if (respCode > 299)
            throw new IOException("Unexpected response code: " + respCode);

        try { // always assume a single item is created or updated.
            JSONObject respObj = parseJSON(resp);
            int deletedCount = respObj.getInt("numDeletedBookmarks");

            if (deletedCount < 1)
                throw new NotFoundException("Bookmark could not be found; " + googleId);
            if (deletedCount > 1)
                throw new IOException("Expected 1 deleted bookmark but got " + deletedCount);
        } catch (JSONException ex) {
            throw new IOException("Response parse error", ex);
        }
    }

    protected Bookmark createOrUpdate(String url, JSONObject requestObj) throws AuthException, IOException {
        HttpPost post = new HttpPost(url);

        //      Log.v(TAG, "UPDATE: " + url);
        //      Log.v(TAG, "UPDATE: " + requestObj);
        List<NameValuePair> params = new ArrayList<NameValuePair>();
        params.add(new BasicNameValuePair("td", requestObj.toString()));
        post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
        HttpResponse resp = http.execute(post, this.ctx);

        int respCode = resp.getStatusLine().getStatusCode();
        if (respCode == 401)
            throw new AuthException();
        if (respCode > 299)
            throw new IOException("Unexpected response code: " + respCode);

        try { // always assume a single item is created or updated.
            JSONObject respObj = parseJSON(resp);
            if (respObj.has("results")) // create response:
                respObj = respObj.getJSONArray("results").getJSONObject(0).getJSONObject("threadresult");
            else
                respObj = respObj.getJSONArray("threadResults").getJSONObject(0);
            Bookmark b = new Bookmark(respObj.getString("elementId"), respObj.getString("threadId"),
                    respObj.getString("title"), respObj.getString("url"), respObj.getString("host"),
                    respObj.getString("snippet"), -1, // no created date in response
                    respObj.getLong("timestamp"));

            if (respObj.has("faviconUrl"))
                b.setFaviconURL(respObj.getString("faviconUrl"));

            //         Log.v(TAG, "RESPONSE: " + respObj );
            if (respObj.has("labels")) {
                JSONArray labelJSON = respObj.getJSONArray("labels");

                for (int i = 0; i < labelJSON.length(); i++)
                    b.getLabels().add(labelJSON.getString(i));
            }

            return b;
        } catch (JSONException ex) {
            Log.w(TAG, "Response parse error", ex);
            throw new IOException("Response parse error");
        }
    }

    protected String getXtParam() throws AuthException, IOException {
        if (this.xtParam != null)
            return this.xtParam; // already init'd

        HttpGet get = new HttpGet("https://www.google.com/bookmarks/l");

        HttpResponse resp = http.execute(get, this.ctx);

        final int responseCode = resp.getStatusLine().getStatusCode();
        if (responseCode == 401)
            throw new AuthException("Please log in");
        if (responseCode != 200)
            throw new IOException("Unexpected response code: " + responseCode);

        // TODO encoding
        final String respString = IOUtils.toString(resp.getEntity().getContent());

        final String xtSearchString = ";SL.xt = '";
        int startIndex = respString.indexOf(xtSearchString);
        if (startIndex < 0)
            throw new IOException("Could not find xtSearchString");
        startIndex += xtSearchString.length();
        this.xtParam = respString.substring(startIndex, respString.indexOf("'", startIndex));
        //      Log.d(TAG, "XT context: " + respString.substring( startIndex-10, 
        //            respString.indexOf("'", startIndex)+5 ) );
        Log.d(TAG, "GOT XT PARAM: " + xtParam);

        // Get main thread ID:
        final String mainThreadSearchString = "(a.threadID):\"";
        startIndex = respString.indexOf(mainThreadSearchString);
        if (startIndex < 0)
            throw new IOException("Could not find thread ID");
        startIndex += mainThreadSearchString.length();
        this.mainThreadId = respString.substring(startIndex, respString.indexOf("\"", startIndex));
        Log.d(TAG, "GOT THREAD ID: " + mainThreadId);

        return this.xtParam;
    }

    protected JSONObject queryJSON(String uri) throws AuthException, JSONException, IOException {
        HttpGet get = new HttpGet(uri);

        HttpResponse resp = http.execute(get, this.ctx);
        int code = resp.getStatusLine().getStatusCode();
        if (code == 401 || code == 403) {
            Log.d(TAG, "Auth failure from queryJSON");
            throw new AuthException();
        }
        if (code != 200) {
            Log.e(TAG, "Unexpected response code: " + code);
            throw new IOException("Unexpected response code: " + code);
        }
        return parseJSON(resp);
    }

    protected JSONObject parseJSON(HttpResponse resp) throws IOException, JSONException {
        String charset = null;
        try {
            charset = resp.getEntity().getContentType().getElements()[0].getParameterByName("charset").getValue();
        } catch (Exception ex) {
            charset = "UTF-8";
        }
        String respData = IOUtils.toString(resp.getEntity().getContent(), charset);
        resp.getEntity().consumeContent();
        if (respData.startsWith(")]}'"))
            respData = respData.substring(respData.indexOf("\n"));

        JSONTokener parser = new JSONTokener(respData);
        return (JSONObject) parser.nextValue();
    }

    public List<Label> getLabels() throws IOException {
        String uri = "https://www.google.com/bookmarks/api/bookmark?op=LIST_LABELS";

        try {
            JSONObject labelsObj = queryJSON(uri);
            JSONArray labels = labelsObj.getJSONArray("labels");
            JSONArray counts = labelsObj.getJSONArray("counts");

            ArrayList<Label> list = new ArrayList<Label>();
            for (int i = 0; i < labels.length(); i++)
                list.add(new Label(labels.getString(i), counts.getInt(i)));

            return list;
        } catch (JSONException ex) {
            Log.e(TAG, "Labels query JSON parse exception", ex);
            throw new IOException("Error retrieving labels", ex);
        }
    }

    public Iterable<BookmarkList> getMyBookmarks() throws AuthException, IOException {
        return new BookmarkListIterator(this, BookmarkList.LISTS_PRIVATE);
    }

    public Iterable<BookmarkList> getSharedBookmarks() throws AuthException, IOException {
        return new BookmarkListIterator(this, BookmarkList.LISTS_SHARED);
    }

    public Iterable<BookmarkList> getPublishedBookmarks() throws AuthException, IOException {
        return new BookmarkListIterator(this, BookmarkList.LISTS_PUBLIC);
    }

    /**
     * This has the potential to be relatively large..
     */
    public Iterable<Bookmark> getAllBookmarks() throws AuthException, IOException {
        // make a sequence of JSON requests until they've all been retrieved.
        // max 25 bookmarks can be requested at one time.
        return new AllBookmarksIterator(this);
    }
}