/* * Copyright (C) 2009 Muthu Ramadoss. All rights reserved. * * Modified from Romain Guy Shelves project to suit Books-Exchange requirements. * Original source from Shelves - http://code.google.com/p/shelves/ */ /* * Copyright (C) 2008 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. */ package com.androidrocks.bex.provider; import android.content.ContentValues; import android.database.Cursor; import android.graphics.Bitmap; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; import android.provider.BaseColumns; import android.text.TextUtils; import org.apache.http.HttpEntity; import org.apache.http.HttpHost; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.methods.HttpGet; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import com.androidrocks.bex.util.HttpManager; import com.androidrocks.bex.util.ImageUtilities; import com.androidrocks.bex.util.TextUtilities; import java.io.IOException; import java.io.InputStream; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Calendar; import java.util.GregorianCalendar; /** * Utility class to load books from a books store. */ public abstract class BooksStore { static final String LOG_TAG = "Shelves"; private final String mStoreName; private final String mStoreLabel; private final String mHost; public enum ImageSize { // SWATCH, // SMALL, THUMBNAIL, TINY, // MEDIUM, // LARGE } public static class Description { private String mSource; private String mContent; Description(String source, String content) { mSource = source; mContent = content; } String getSource() { return mSource; } String getContent() { return mContent; } @Override public String toString() { // TODO: We should be storing reviews in a separate table return "<p class=\".source\">" + mSource + "</p>\n<p class=\".content\">" + mContent + "</p>"; } } public static class Book implements Parcelable, BaseColumns { public static final Uri CONTENT_URI = Uri.parse("content://shelves/books"); public static final String DEFAULT_SORT_ORDER = "sort_title ASC"; public static final String INTERNAL_ID = "internal_id"; public static final String EAN = "ean"; public static final String ISBN = "isbn"; public static final String TITLE = "title"; public static final String SORT_TITLE = "sort_title"; public static final String AUTHORS = "authors"; public static final String PUBLISHER = "publisher"; public static final String REVIEWS = "reviews"; public static final String PAGES = "pages"; public static final String LAST_MODIFIED = "last_modified"; public static final String PUBLICATION = "publication"; public static final String DETAILS_URL = "details_url"; public static final String TINY_URL = "tiny_url"; String mIsbn; String mEan; String mInternalId; Map<ImageSize, String> mImages; List<String> mAuthors; int mPages; String mTitle; Date mPublicationDate; List<Description> mDescriptions; String mDetailsUrl; String mPublisher; Calendar mLastModified; private String mStorePrefix; private ImageLoader mLoader; Book() { this("", null); } Book(String storePrefix, ImageLoader loader) { mStorePrefix = storePrefix; mLoader = loader; mImages = new HashMap<ImageSize, String>(6); mAuthors = new ArrayList<String>(1); mDescriptions = new ArrayList<Description>(); } private Book(Parcel in) { mIsbn = in.readString(); mEan = in.readString(); mInternalId = in.readString(); mTitle = in.readString(); mAuthors = new ArrayList<String>(1); in.readStringList(mAuthors); } public String getIsbn() { return mIsbn; } public String getEan() { return mEan; } public String getInternalId() { return mStorePrefix + mInternalId; } public String getInternalIdNoPrefix() { return mInternalId; } public List<String> getAuthors() { return mAuthors; } public int getPagesCount() { return mPages; } public String getTitle() { return mTitle; } public Date getPublicationDate() { return mPublicationDate; } public List<Description> getDescriptions() { return mDescriptions; } public String getDetailsUrl() { return mDetailsUrl; } public String getPublisher() { return mPublisher; } public Calendar getLastModified() { return mLastModified; } public String getImageUrl(ImageSize size) { return mImages.get(size); } public Bitmap loadCover(ImageSize size) { final String url = mImages.get(size); if (url == null) return null; final ImageUtilities.ExpiringBitmap expiring; if (mLoader == null) { expiring = ImageUtilities.load(url); } else { expiring = mLoader.load(url); } mLastModified = expiring.lastModified; return expiring.bitmap; } public ContentValues getContentValues() { final SimpleDateFormat format = new SimpleDateFormat("MMMM yyyy"); final ContentValues values = new ContentValues(); values.put(INTERNAL_ID, mStorePrefix + mInternalId); values.put(EAN, mEan); values.put(ISBN, mIsbn); values.put(TITLE, mTitle); values.put(AUTHORS, TextUtilities.join(mAuthors, ", ")); values.put(PUBLISHER, mPublisher); values.put(REVIEWS, TextUtilities.join(mDescriptions, "\n\n")); values.put(PAGES, mPages); if (mLastModified != null) { values.put(LAST_MODIFIED, mLastModified.getTimeInMillis()); } values.put(PUBLICATION, mPublicationDate != null ? format.format(mPublicationDate) : ""); values.put(DETAILS_URL, mDetailsUrl); values.put(TINY_URL, mImages.get(ImageSize.TINY)); return values; } public static Book fromCursor(Cursor c) { final Book book = new Book(); book.mInternalId = c.getString(c.getColumnIndexOrThrow(INTERNAL_ID)); book.mEan = c.getString(c.getColumnIndexOrThrow(EAN)); book.mIsbn = c.getString(c.getColumnIndexOrThrow(ISBN)); book.mTitle = c.getString(c.getColumnIndexOrThrow(TITLE)); Collections.addAll(book.mAuthors, c.getString(c.getColumnIndexOrThrow(AUTHORS)).split(", ")); book.mPublisher = c.getString(c.getColumnIndexOrThrow(PUBLISHER)); book.mDescriptions.add(new Description("", c.getString(c.getColumnIndexOrThrow(REVIEWS)))); book.mPages = c.getInt(c.getColumnIndexOrThrow(PAGES)); final Calendar calendar = GregorianCalendar.getInstance(); calendar.setTimeInMillis(c.getLong(c.getColumnIndexOrThrow(LAST_MODIFIED))); book.mLastModified = calendar; final SimpleDateFormat format = new SimpleDateFormat("MMMM yyyy"); try { book.mPublicationDate = format.parse(c.getString( c.getColumnIndexOrThrow(PUBLICATION))); } catch (ParseException e) { // Ignore } book.mDetailsUrl = c.getString(c.getColumnIndexOrThrow(DETAILS_URL)); book.mImages.put(ImageSize.TINY, c.getString(c.getColumnIndexOrThrow(TINY_URL))); return book; } @Override public String toString() { return "Book[ISBN=" + mIsbn + ", EAN=" + mEan + ", IID=" + mInternalId + "]"; } public int describeContents() { return 0; } public void writeToParcel(Parcel dest, int flags) { dest.writeString(mIsbn); dest.writeString(mEan); dest.writeString(mInternalId); dest.writeString(mTitle); dest.writeStringList(mAuthors); } public static final Creator<Book> CREATOR = new Creator<Book>() { public Book createFromParcel(Parcel in) { return new Book(in); } public Book[] newArray(int size) { return new Book[size]; } }; } BooksStore(String name, String label, String host) { mStoreName = name; mStoreLabel = label; mHost = host; } public String getName() { return mStoreName; } public String getLabel() { return mStoreLabel; } /** * Finds the book with the specified id. * * @param id The id of the book to find (ISBN-10, ISBN-13, etc.) * * @return A Book instance if the book was found or null otherwise. */ public Book findBook(String id) { final Uri.Builder uri = buildFindBookQuery(id); final HttpGet get = new HttpGet(uri.build().toString()); final Book book = createBook(); final boolean[] result = new boolean[1]; try { executeRequest(new HttpHost(mHost, 80, "http"), get, new ResponseHandler() { public void handleResponse(InputStream in) throws IOException { parseResponse(in, new ResponseParser() { public void parseResponse(XmlPullParser parser) throws XmlPullParserException, IOException { result[0] = parseBook(parser, book); } }); } }); if (TextUtils.isEmpty(book.mEan) && id.length() == 13) { book.mEan = id; } else if (TextUtils.isEmpty(book.mIsbn) && id.length() == 10) { book.mIsbn = id; } return result[0] ? book : null; } catch (IOException e) { android.util.Log.e(LOG_TAG, "Could not find the item with ISBN/EAN: " + id); } return null; } /** * Searchs for books that match the provided query. * * @param query The free form query used to search for books. * * @return A list of Book instances if query was successful or null otherwise. */ public ArrayList<Book> searchBooks(String query, final BookSearchListener listener) { final Uri.Builder uri = buildSearchBooksQuery(query); final HttpGet get = new HttpGet(uri.build().toString()); final ArrayList<Book> books = new ArrayList<Book>(10); try { executeRequest(new HttpHost(mHost, 80, "http"), get, new ResponseHandler() { public void handleResponse(InputStream in) throws IOException { parseResponse(in, new ResponseParser() { public void parseResponse(XmlPullParser parser) throws XmlPullParserException, IOException { parseBooks(parser, books, listener); } }); } }); return books; } catch (IOException e) { android.util.Log.e(LOG_TAG, "Could not perform search with query: " + query, e); } return null; } /** * Constructs the query used to search for books. The query can be any combination * of keywords. The store is free to interpret the keywords in any way. * * @param query A free form text query to search for books. * * @return The Uri to the list of books matching the query. */ abstract Uri.Builder buildSearchBooksQuery(String query); /** * Constructs the query used to find a book identified by its id. The unique * identifier should be either the EAN (ISBN-13) or ISBN (ISBN-10) of the book * to find. * * @param id The EAN or ISBN of the book to find. * * @return The Uri to the books details for this book store. */ abstract Uri.Builder buildFindBookQuery(String id); /** * Parses a valid XML response from the specified input stream. This method must * invoke parse{@link ResponseParser#parseResponse(org.xmlpull.v1.XmlPullParser)} if * the XML response is valid, or throw an exception if it is not. * * @param in The input stream containing the response sent by the web service. * @param responseParser The parser to use when the response is valid. * * @throws java.io.IOException */ abstract void parseResponse(InputStream in, ResponseParser responseParser) throws IOException; /** * Parses a book from the XML input stream. * * @param parser The XML parser to use to parse the book. * @param book The book object to put the parsed data in. * * @return True if the book could correctly be parsed, false otherwise. */ abstract boolean parseBook(XmlPullParser parser, Book book) throws XmlPullParserException, IOException; /** * Finds the next book entry in the XML input stream. * * @param parser The XML parser to use to parse the book. * * @return True if a book was found, false otherwise. */ abstract boolean findNextBook(XmlPullParser parser) throws XmlPullParserException, IOException; /** * Creates an instance of {@link com.androidrocks.bex.provider.BooksStore.Book} * with this book store's name. * * @return A new instance of Book. */ Book createBook() { return new Book(getName(), null); } private void parseBooks(XmlPullParser parser, ArrayList<Book> books, BookSearchListener listener) throws IOException, XmlPullParserException { int type; while ((type = parser.next()) != XmlPullParser.END_TAG && type != XmlPullParser.END_DOCUMENT) { if (type != XmlPullParser.START_TAG) { continue; } if (findNextBook(parser)) { final Book book = createBook(); if (parseBook(parser, book)) { books.add(book); listener.onBookFound(book, books); } } } } /** * Executes an HTTP request on a REST web service. If the response is ok, the content * is sent to the specified response handler. * * @param host * @param get The GET request to executed. * @param handler The handler which will parse the response. * * @throws java.io.IOException */ private void executeRequest(HttpHost host, HttpGet get, ResponseHandler handler) throws IOException { HttpEntity entity = null; try { final HttpResponse response = HttpManager.execute(host, get); if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { entity = response.getEntity(); final InputStream in = entity.getContent(); handler.handleResponse(in); } } finally { if (entity != null) { entity.consumeContent(); } } } /** * Response handler used with {@link BooksStore#executeRequest(org.apache.http.HttpHost, * org.apache.http.client.methods.HttpGet, BooksStore.ResponseHandler)}. * The handler is invoked when a response is sent by the server. The response is made * available as an input stream. */ static interface ResponseHandler { /** * Processes the responses sent by the HTTP server following a GET request. * * @param in The stream containing the server's response. * * @throws java.io.IOException */ public void handleResponse(InputStream in) throws IOException; } /** * Response parser. When the request returns a valid response, this parser * is invoked to process the XML response. */ static interface ResponseParser { /** * Processes the XML response sent by the web service after a successful request. * * @param parser The parser containing the XML responses. * * @throws org.xmlpull.v1.XmlPullParserException * @throws java.io.IOException */ public void parseResponse(XmlPullParser parser) throws XmlPullParserException, IOException; } /** * Interface used to load images with an expiring date. The expiring date is handled by * the image cache to check for updated images from time to time. */ static interface ImageLoader { /** * Load the specified URL as a Bitmap and associates an expiring date to it. * * @param url The URL of the image to load. * * @return The Bitmap decoded from the URL and an expiration date. */ public ImageUtilities.ExpiringBitmap load(String url); } /** * Listener invoked by * {@link com.androidrocks.bex.provider.BooksStore#searchBooks(String, * com.androidrocks.bex.provider.BooksStore.BookSearchListener)}. */ public static interface BookSearchListener { /** * Invoked whenever a book was found by the search operation. * * @param book The book yield by the search query. * @param books The books found so far, including <code>book</code>. */ void onBookFound(Book book, ArrayList<Book> books); } }