/*
* @copyright 2012 Philip Warner
* @license GNU General Public License
*
* This file is part of Book Catalogue.
*
* Book Catalogue 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.
*
* Book Catalogue 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 Book Catalogue. If not, see <http://www.gnu.org/licenses/>.
*/
package com.eleybourn.bookcatalogue.goodreads.api;
import java.io.IOException;
import oauth.signpost.exception.OAuthCommunicationException;
import oauth.signpost.exception.OAuthExpectationFailedException;
import oauth.signpost.exception.OAuthMessageSignerException;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpGet;
import android.os.Bundle;
import com.eleybourn.bookcatalogue.goodreads.GoodreadsManager;
import com.eleybourn.bookcatalogue.goodreads.GoodreadsManager.Exceptions.BookNotFoundException;
import com.eleybourn.bookcatalogue.goodreads.GoodreadsManager.Exceptions.NetworkException;
import com.eleybourn.bookcatalogue.goodreads.GoodreadsManager.Exceptions.NotAuthorizedException;
/**
* Class to implement the reviews.list api call. It queries based on the passed parameters and returns
* a single Bundle containing all results. The Bundle itself will contain other bundles: typically an
* array of 'Review' bundles, each of which will contains arrays of 'author' bundles.
*
* Processing this data is up to the caller, but it is guaranteed to be type-safe if present, with the
* exception of dates, which are collected as text strings.
*
* @author Philip Warner
*/
public class BookshelfListApiHandler extends ApiHandler {
/**
* Field names we add to the bundle based on parsed XML data.
*
* We duplicate the CatalogueDBAdapter names (and give them a DB_ prefix) so
* that (a) it is clear which fields are provided by this call, and (b) it is clear
* which fields directly relate to DB fields.
*
* @author Philip Warner
*/
public static final class BookshelfListFieldNames {
public static final String SHELVES = "shelves";
public static final String SHELF = "shelf";
public static final String START = "start";
public static final String END = "end";
public static final String TOTAL = "total";
public static final String EXCLUSIVE = "exclusive";
public static final String ID = "id";
public static final String NAME = "name";
}
private SimpleXmlFilter mFilters;
public BookshelfListApiHandler(GoodreadsManager manager) {
super(manager);
if (!manager.hasValidCredentials())
throw new RuntimeException("Goodreads credentials not valid");
// Build the XML filters needed to get the data we're interested in.
buildFilters();
}
/**
*
* @param page
* @return
* @throws ClientProtocolException
* @throws OAuthMessageSignerException
* @throws OAuthExpectationFailedException
* @throws OAuthCommunicationException
* @throws NotAuthorizedException
* @throws BookNotFoundException
* @throws IOException
* @throws NetworkException
*/
public Bundle run(int page)
throws ClientProtocolException, OAuthMessageSignerException, OAuthExpectationFailedException,
OAuthCommunicationException, NotAuthorizedException, BookNotFoundException, IOException, NetworkException
{
long t0 = System.currentTimeMillis();
// Sort by update_dte (descending) so sync is faster. Specify 'shelf=all' because it seems goodreads returns
// the shelf that is selected in 'My Books' on the web interface by default.
final String urlBase = "http://www.goodreads.com/shelf/list.xml?key=%1$s&page=%2$s&user_id=%3$s";
final String url = String.format(urlBase, mManager.getDeveloperKey(), page, mManager.getUserid());
HttpGet get = new HttpGet(url);
// Inital debug code:
//TrivialParser handler = new TrivialParser();
//mManager.execute(get, handler, true);
//String s = handler.getHtml();
//System.out.println(s);
// Get a handler and run query.
XmlResponseParser handler = new XmlResponseParser(mRootFilter);
// Even thought it's only a GET, it needs a signature.
mManager.execute(get, handler, true);
// When we get here, the data has been collected but needs to be processed into standard form.
Bundle results = mFilters.getData();
// Return parsed results.
long t1 = System.currentTimeMillis();
System.out.println("Found " + results.getLong(BookshelfListFieldNames.TOTAL) + " shelves in " + (t1 - t0) + "ms");
return results;
}
/*
* Typical result:
<GoodreadsResponse>
<Request>
<authentication>true</authentication>
<key>....</key>
<method>shelf_list</method>
</Request>
<shelves start='1' end='29' total='29'>
<user_shelf>
<book_count type='integer'>546</book_count>
<description nil='true'></description>
<display_fields></display_fields>
<exclusive_flag type='boolean'>true</exclusive_flag>
<featured type='boolean'>false</featured>
<id type='integer'>16480894</id>
<name>read</name>
<order nil='true'></order>
<per_page type='integer' nil='true'></per_page>
<recommend_for type='boolean'>false</recommend_for>
<sort></sort>
<sticky type='boolean' nil='true'></sticky>
</user_shelf>
<user_shelf>
<book_count type='integer'>0</book_count>
<description nil='true'></description>
<display_fields></display_fields>
<exclusive_flag type='boolean'>true</exclusive_flag>
<featured type='boolean'>false</featured>
<id type='integer'>16480900</id>
<name>currently-reading</name>
<order nil='true'></order>
<per_page type='integer' nil='true'></per_page>
<recommend_for type='boolean'>false</recommend_for>
<sort nil='true'></sort>
<sticky type='boolean' nil='true'></sticky>
</user_shelf>
<user_shelf>
<book_count type='integer'>381</book_count>
<description nil='true'></description>
<display_fields></display_fields>
<exclusive_flag type='boolean'>true</exclusive_flag>
<featured type='boolean'>false</featured>
<id type='integer'>16480892</id>
<name>to-read</name>
<order>a</order>
<per_page type='integer' nil='true'></per_page>
<recommend_for type='boolean'>false</recommend_for>
<sort>position</sort>
<sticky type='boolean' nil='true'></sticky>
</user_shelf>
<user_shelf>
<book_count type='integer'>5</book_count>
<description nil='true'></description>
<display_fields></display_fields>
<exclusive_flag type='boolean'>false</exclusive_flag>
<featured type='boolean'>false</featured>
<id type='integer'>26567684</id>
<name>aabug</name>
<order nil='true'></order>
<per_page type='integer' nil='true'></per_page>
<recommend_for type='boolean'>true</recommend_for>
<sort nil='true'></sort>
<sticky type='boolean' nil='true'></sticky>
</user_shelf>
<user_shelf>
<book_count type='integer'>6</book_count>
<description nil='true'></description>
<display_fields></display_fields>
<exclusive_flag type='boolean'>false</exclusive_flag>
<featured type='boolean'>false</featured>
<id type='integer'>24457791</id>
<name>anthologies</name>
<order nil='true'></order>
<per_page type='integer' nil='true'></per_page>
<recommend_for type='boolean'>true</recommend_for>
<sort nil='true'></sort>
<sticky type='boolean' nil='true'></sticky>
</user_shelf>
</shelves>
</GoodreadsResponse>
*/
/**
* Setup filters to process the XML parts we care about.
*/
protected void buildFilters() {
/*
* Process the stuff we care about
*/
mFilters = new SimpleXmlFilter(mRootFilter);
mFilters
//<GoodreadsResponse>
.s("GoodreadsResponse")
// <Request>
// ...
// </Request>
// <reviews start="3" end="4" total="933">
// <shelves start='1' end='29' total='29'>
.s("shelves").isArray(BookshelfListFieldNames.SHELVES)
.longAttr("start", BookshelfListFieldNames.START)
.longAttr("end", BookshelfListFieldNames.END)
.longAttr("total", BookshelfListFieldNames.TOTAL)
// <user_shelf>
.s("user_shelf").isArrayItem()
// <exclusive_flag type='boolean'>false</exclusive_flag>
.booleanBody("exclusive_flag", BookshelfListFieldNames.EXCLUSIVE)
// <id type='integer'>26567684</id>
.longBody("id", BookshelfListFieldNames.ID)
.stringBody("name", BookshelfListFieldNames.NAME)
.pop()
.done();
}
// /**
// * Listener to handle the contents of the date_updated field. We only
// * keep it if it is a valid date, and we store it in SQL format using
// * UTC TZ so comparisons work.
// */
// XmlListener mUpdatedListener = new XmlListener() {
// @Override
// public void onStart(BuilderContext bc, ElementContext c) {
// }
//
// @Override
// public void onFinish(BuilderContext bc, ElementContext c) {
// date2Sql(bc.getData(), UPDATED);
// }
// };
//
// /**
// * Listener to handle the contents of the date_added field. We only
// * keep it if it is a valid date, and we store it in SQL format using
// * UTC TZ so comparisons work.
// */
// XmlListener mAddedListener = new XmlListener() {
// @Override
// public void onStart(BuilderContext bc, ElementContext c) {
// }
//
// @Override
// public void onFinish(BuilderContext bc, ElementContext c) {
// date2Sql(bc.getData(), ADDED);
// }
// };
}