/*
* @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 static com.eleybourn.bookcatalogue.goodreads.api.ListReviewsApiHandler.ListReviewsFieldNames.ADDED;
import static com.eleybourn.bookcatalogue.goodreads.api.ListReviewsApiHandler.ListReviewsFieldNames.AUTHORS;
import static com.eleybourn.bookcatalogue.goodreads.api.ListReviewsApiHandler.ListReviewsFieldNames.DB_AUTHOR_ID;
import static com.eleybourn.bookcatalogue.goodreads.api.ListReviewsApiHandler.ListReviewsFieldNames.DB_AUTHOR_NAME;
import static com.eleybourn.bookcatalogue.goodreads.api.ListReviewsApiHandler.ListReviewsFieldNames.DB_DESCRIPTION;
import static com.eleybourn.bookcatalogue.goodreads.api.ListReviewsApiHandler.ListReviewsFieldNames.DB_FORMAT;
import static com.eleybourn.bookcatalogue.goodreads.api.ListReviewsApiHandler.ListReviewsFieldNames.DB_ISBN;
import static com.eleybourn.bookcatalogue.goodreads.api.ListReviewsApiHandler.ListReviewsFieldNames.DB_NOTES;
import static com.eleybourn.bookcatalogue.goodreads.api.ListReviewsApiHandler.ListReviewsFieldNames.DB_PAGES;
import static com.eleybourn.bookcatalogue.goodreads.api.ListReviewsApiHandler.ListReviewsFieldNames.DB_PUBLISHER;
import static com.eleybourn.bookcatalogue.goodreads.api.ListReviewsApiHandler.ListReviewsFieldNames.DB_RATING;
import static com.eleybourn.bookcatalogue.goodreads.api.ListReviewsApiHandler.ListReviewsFieldNames.DB_READ_END;
import static com.eleybourn.bookcatalogue.goodreads.api.ListReviewsApiHandler.ListReviewsFieldNames.DB_READ_START;
import static com.eleybourn.bookcatalogue.goodreads.api.ListReviewsApiHandler.ListReviewsFieldNames.DB_TITLE;
import static com.eleybourn.bookcatalogue.goodreads.api.ListReviewsApiHandler.ListReviewsFieldNames.END;
import static com.eleybourn.bookcatalogue.goodreads.api.ListReviewsApiHandler.ListReviewsFieldNames.GR_BOOK_ID;
import static com.eleybourn.bookcatalogue.goodreads.api.ListReviewsApiHandler.ListReviewsFieldNames.GR_REVIEW_ID;
import static com.eleybourn.bookcatalogue.goodreads.api.ListReviewsApiHandler.ListReviewsFieldNames.ISBN13;
import static com.eleybourn.bookcatalogue.goodreads.api.ListReviewsApiHandler.ListReviewsFieldNames.LARGE_IMAGE;
import static com.eleybourn.bookcatalogue.goodreads.api.ListReviewsApiHandler.ListReviewsFieldNames.PUB_DAY;
import static com.eleybourn.bookcatalogue.goodreads.api.ListReviewsApiHandler.ListReviewsFieldNames.PUB_MONTH;
import static com.eleybourn.bookcatalogue.goodreads.api.ListReviewsApiHandler.ListReviewsFieldNames.PUB_YEAR;
import static com.eleybourn.bookcatalogue.goodreads.api.ListReviewsApiHandler.ListReviewsFieldNames.REVIEWS;
import static com.eleybourn.bookcatalogue.goodreads.api.ListReviewsApiHandler.ListReviewsFieldNames.SHELF;
import static com.eleybourn.bookcatalogue.goodreads.api.ListReviewsApiHandler.ListReviewsFieldNames.SHELVES;
import static com.eleybourn.bookcatalogue.goodreads.api.ListReviewsApiHandler.ListReviewsFieldNames.SMALL_IMAGE;
import static com.eleybourn.bookcatalogue.goodreads.api.ListReviewsApiHandler.ListReviewsFieldNames.START;
import static com.eleybourn.bookcatalogue.goodreads.api.ListReviewsApiHandler.ListReviewsFieldNames.TOTAL;
import static com.eleybourn.bookcatalogue.goodreads.api.ListReviewsApiHandler.ListReviewsFieldNames.UPDATED;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
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.CatalogueDBAdapter;
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;
import com.eleybourn.bookcatalogue.goodreads.api.SimpleXmlFilter.BuilderContext;
import com.eleybourn.bookcatalogue.goodreads.api.SimpleXmlFilter.XmlListener;
import com.eleybourn.bookcatalogue.goodreads.api.XmlFilter.ElementContext;
import com.eleybourn.bookcatalogue.utils.Utils;
/**
* 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 ListReviewsApiHandler extends ApiHandler {
/** Date format used for parsing 'last_update_date' */
private static final SimpleDateFormat mUpdateDateFmt = new SimpleDateFormat("EEE MMM dd HH:mm:ss ZZZZ yyyy");
/**
* 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 ListReviewsFieldNames {
public static final String START = "__start";
public static final String END = "__end";
public static final String TOTAL = "__total";
public static final String GR_BOOK_ID = "__gr_book_id";
public static final String GR_REVIEW_ID = "__gr_review_id";
public static final String ISBN13 = "__isbn13";
public static final String SMALL_IMAGE = "__smallImage";
public static final String LARGE_IMAGE = "__largeImage";
public static final String PUB_DAY = "__pubDay";
public static final String PUB_YEAR = "__pubYear";
public static final String PUB_MONTH = "__pubMonth";
public static final String ADDED = "__added";
public static final String UPDATED = "__updated";
public static final String REVIEWS = "__reviews";
public static final String AUTHORS = "__authors";
public static final String SHELF = "__shelf";
public static final String SHELVES = "__shelves";
public static final String DB_PAGES = CatalogueDBAdapter.KEY_PAGES;
public static final String DB_ISBN = CatalogueDBAdapter.KEY_ISBN;
public static final String DB_TITLE = CatalogueDBAdapter.KEY_TITLE;
public static final String DB_NOTES = CatalogueDBAdapter.KEY_NOTES;
public static final String DB_FORMAT = CatalogueDBAdapter.KEY_FORMAT;
public static final String DB_PUBLISHER = CatalogueDBAdapter.KEY_PUBLISHER;
public static final String DB_DESCRIPTION = CatalogueDBAdapter.KEY_DESCRIPTION;
public static final String DB_AUTHOR_ID = CatalogueDBAdapter.KEY_AUTHOR_ID;
public static final String DB_AUTHOR_NAME = CatalogueDBAdapter.KEY_AUTHOR_NAME;
public static final String DB_RATING = CatalogueDBAdapter.KEY_RATING;
public static final String DB_READ_START = CatalogueDBAdapter.KEY_READ_START;
public static final String DB_READ_END = CatalogueDBAdapter.KEY_READ_END;
}
private SimpleXmlFilter mFilters;
public ListReviewsApiHandler(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, int perPage)
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/review/list/%4$s.xml?key=%1$s&v=2&page=%2$s&per_page=%3$s&sort=date_updated&order=d&shelf=all";
final String url = String.format(urlBase, mManager.getDeveloperKey(), page, perPage, mManager.getUserid());
HttpGet get = new HttpGet(url);
// 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(TOTAL) + " books in " + (t1 - t0) + "ms");
return results;
}
/*
* Typical result:
<GoodreadsResponse>
<Request>
...
</Request>
<reviews start="3" end="4" total="933">
<review>
<id>276860380</id>
<book>
<id type="integer">951750</id>
<isbn>0583120911</isbn>
<isbn13>9780583120913</isbn13>
<text_reviews_count type="integer">2</text_reviews_count>
<title>
<![CDATA[The Dying Earth]]>
</title>
<image_url>http://photo.goodreads.com/books/1294108593m/951750.jpg</image_url>
<small_image_url>http://photo.goodreads.com/books/1294108593s/951750.jpg</small_image_url>
<link>http://www.goodreads.com/book/show/951750.The_Dying_Earth</link>
<num_pages>159</num_pages>
<format></format>
<edition_information></edition_information>
<publisher></publisher>
<publication_day>20</publication_day>
<publication_year>1972</publication_year>
<publication_month>4</publication_month>
<average_rating>3.99</average_rating>
<ratings_count>713</ratings_count>
<description>
<![CDATA[]]>
</description>
<authors>
<author>
<id>5376</id>
<name><![CDATA[Jack Vance]]></name>
<image_url><![CDATA[http://photo.goodreads.com/authors/1207604643p5/5376.jpg]]></image_url>
<small_image_url><![CDATA[http://photo.goodreads.com/authors/1207604643p2/5376.jpg]]></small_image_url>
<link><![CDATA[http://www.goodreads.com/author/show/5376.Jack_Vance]]></link>
<average_rating>3.94</average_rating>
<ratings_count>12598</ratings_count>
<text_reviews_count>844</text_reviews_count>
</author>
</authors>
<published>1972</published>
</book>
<rating>0</rating>
<votes>0</votes>
<spoiler_flag>false</spoiler_flag>
<spoilers_state>none</spoilers_state>
<shelves>
<shelf name="sci-fi-fantasy" />
<shelf name="to-read" />
</shelves>
<recommended_for><![CDATA[]]></recommended_for>
<recommended_by><![CDATA[]]></recommended_by>
<started_at></started_at>
<read_at></read_at>
<date_added>Mon Feb 13 05:32:30 -0800 2012</date_added>
<date_updated>Mon Feb 13 05:32:31 -0800 2012</date_updated>
<read_count></read_count>
<body>
<![CDATA[]]>
</body>
<comments_count>0</comments_count>
<url><![CDATA[http://www.goodreads.com/review/show/276860380]]></url>
<link><![CDATA[http://www.goodreads.com/review/show/276860380]]></link>
<owned>0</owned>
</review>
<review>
<id>273090417</id>
<book>
<id type="integer">2042540</id>
<isbn>0722129203</isbn>
<isbn13>9780722129203</isbn13>
<text_reviews_count type="integer">0</text_reviews_count>
<title>
<![CDATA[The Fallible Fiend]]>
</title>
<image_url>http://www.goodreads.com/images/nocover-111x148.jpg</image_url>
<small_image_url>http://www.goodreads.com/images/nocover-60x80.jpg</small_image_url>
<link>http://www.goodreads.com/book/show/2042540.The_Fallible_Fiend</link>
<num_pages></num_pages>
<format></format>
<edition_information></edition_information>
<publisher></publisher>
<publication_day></publication_day>
<publication_year></publication_year>
<publication_month></publication_month>
<average_rating>3.55</average_rating>
<ratings_count>71</ratings_count>
<description>
<![CDATA[]]>
</description>
<authors>
<author>
<id>3305</id>
<name><![CDATA[L. Sprague de Camp]]></name>
<image_url><![CDATA[http://photo.goodreads.com/authors/1218217726p5/3305.jpg]]></image_url>
<small_image_url><![CDATA[http://photo.goodreads.com/authors/1218217726p2/3305.jpg]]></small_image_url>
<link><![CDATA[http://www.goodreads.com/author/show/3305.L_Sprague_de_Camp]]></link>
<average_rating>3.78</average_rating>
<ratings_count>9424</ratings_count>
<text_reviews_count>441</text_reviews_count>
</author>
</authors>
<published></published>
</book>
<rating>0</rating>
<votes>0</votes>
<spoiler_flag>false</spoiler_flag>
<spoilers_state>none</spoilers_state>
<shelves>
<shelf name="read" />
<shelf name="sci-fi-fantasy" />
</shelves>
<recommended_for><![CDATA[]]></recommended_for>
<recommended_by><![CDATA[]]></recommended_by>
<started_at></started_at>
<read_at></read_at>
<date_added>Mon Feb 06 03:40:52 -0800 2012</date_added>
<date_updated>Mon Feb 06 03:40:52 -0800 2012</date_updated>
<read_count></read_count>
<body>
<![CDATA[]]>
</body>
<comments_count>0</comments_count>
<url><![CDATA[http://www.goodreads.com/review/show/273090417]]></url>
<link><![CDATA[http://www.goodreads.com/review/show/273090417]]></link>
<owned>0</owned>
</review>
</reviews>
</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">
.s("reviews").isArray(REVIEWS)
.longAttr("start", START)
.longAttr("end", END)
.longAttr("total", TOTAL)
// <review>
.s("review").isArrayItem()
// <id>276860380</id>
.longBody("id", GR_REVIEW_ID)
// <book>
.s("book")
// <id type="integer">951750</id>
.longBody("id", GR_BOOK_ID)
// <isbn>0583120911</isbn>
.stringBody("isbn", DB_ISBN)
// <isbn13>9780583120913</isbn13>
.stringBody("isbn13", ISBN13)
// ...
// <title><![CDATA[The Dying Earth]]></title>
.stringBody("title", DB_TITLE)
// <image_url>http://photo.goodreads.com/books/1294108593m/951750.jpg</image_url>
.stringBody("image_url", LARGE_IMAGE)
// <small_image_url>http://photo.goodreads.com/books/1294108593s/951750.jpg</small_image_url>
.stringBody("small_image_url", SMALL_IMAGE)
// ...
// <num_pages>159</num_pages>
.longBody("num_pages", DB_PAGES)
// <format></format>
.stringBody("format", DB_FORMAT)
// <publisher></publisher>
.stringBody("publisher", DB_PUBLISHER)
// <publication_day>20</publication_day>
.longBody("publication_day", PUB_DAY)
// <publication_year>1972</publication_year>
.longBody("publication_year", PUB_YEAR)
// <publication_month>4</publication_month>
.longBody("publication_month", PUB_MONTH)
// <description><![CDATA[]]></description>
.stringBody("description", DB_DESCRIPTION)
// ...
//
// <authors>
.s("authors")
.isArray(AUTHORS)
// <author>
.s("author")
.isArrayItem()
// <id>5376</id>
.longBody("id", DB_AUTHOR_ID)
// <name><![CDATA[Jack Vance]]></name>
.stringBody("name", DB_AUTHOR_NAME)
// ...
// </author>
// </authors>
// ...
// </book>
.popTo("review")
//
// <rating>0</rating>
.doubleBody("rating", DB_RATING)
// ...
// <shelves>
.s("shelves")
.isArray(SHELVES)
// <shelf name="sci-fi-fantasy" />
.s("shelf")
.isArrayItem()
.stringAttr("name", SHELF)
.popTo("review")
// <shelf name="to-read" />
// </shelves>
// ...
// <started_at></started_at>
.stringBody("started_at", DB_READ_START)
// <read_at></read_at>
.stringBody("read_at", DB_READ_END)
// <date_added>Mon Feb 13 05:32:30 -0800 2012</date_added>
//.stringBody("date_added", ADDED)
.s("date_added").stringBody(ADDED).setListener(mAddedListener).pop()
// <date_updated>Mon Feb 13 05:32:31 -0800 2012</date_updated>
.s("date_updated").stringBody(UPDATED).setListener(mUpdatedListener).pop()
// ...
// <body><![CDATA[]]></body>
.stringBody("body", DB_NOTES).pop()
// ...
// <owned>0</owned>
// </review>
// </reviews>
//
//</GoodreadsResponse>
.done();
}
void date2Sql(Bundle b, String key) {
if (b.containsKey(key)) {
String date = b.getString(key);
try {
Date d = mUpdateDateFmt.parse(date);
date = Utils.toSqlDateTime(d);
b.putString(key, date);
} catch (Exception e) {
b.remove(key);
}
}
}
/**
* 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);
}
};
}