biz.varkon.shelvesom.server.ServerInfo.java Source code

Java tutorial

Introduction

Here is the source code for biz.varkon.shelvesom.server.ServerInfo.java

Source

/*
 * Copyright (C) 2010 Garen J. Torikian
 * 
 * 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 biz.varkon.shelvesom.server;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.http.Header;
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.apache.http.impl.client.DefaultHttpClient;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import android.content.Context;
import android.net.Uri;
import android.util.Xml;
import android.view.InflateException;

import biz.varkon.shelvesom.BuildConfig;
import biz.varkon.shelvesom.activity.SettingsActivity;
import biz.varkon.shelvesom.provider.books.BooksStore;
import biz.varkon.shelvesom.util.CookieStore;
import biz.varkon.shelvesom.util.Entities;
import biz.varkon.shelvesom.util.HttpManager;
import biz.varkon.shelvesom.util.IOUtilities;
import biz.varkon.shelvesom.util.ImageUtilities;

public class ServerInfo {
    public static final String NAME = "5h3lv35";

    protected static final String DETAIL_PHRASE = "detail";
    public static final String IMAGE_PHRASE = "image";
    protected static final String IMAGE_PATH = "/images/";

    protected static final String VALUE_SEARCHINDEX_APPAREL = "Apparel";
    protected static final String VALUE_RESPONSEGROUP_LARGE = "Large";
    protected static final String VALUE_RESPONSEGROUP_MEDIUM = "Medium";

    protected static final String RESPONSE_TAG_REQUEST = "Request";
    protected static final String RESPONSE_TAG_ISVALID = "IsValid";
    protected static final String RESPONSE_TAG_ERRORS = "Errors";
    protected static final String RESPONSE_TAG_ASIN = "ASIN";
    protected static final String RESPONSE_TAG_DETAILPAGEURL = "DetailPageURL";
    protected static final String RESPONSE_TAG_ITEMATTRIBUTES = "ItemAttributes";
    protected static final String RESPONSE_TAG_MANUFACTURER = "Manufacturer";
    protected static final String RESPONSE_TAG_FABRICTYPE = "FabricType";
    protected static final String RESPONSE_TAG_BRAND = "Brand";
    protected static final String RESPONSE_TAG_LABEL = "Label";
    protected static final String RESPONSE_TAG_ISBN = "ISBN";
    protected static final String RESPONSE_TAG_EAN = "EAN";
    protected static final String RESPONSE_TAG_UPC = "UPC";
    protected static final String RESPONSE_TAG_SKU = "SKU";
    protected static final String RESPONSE_TAG_FEATURE = "Feature";
    protected static final String RESPONSE_TAG_FORMAT = "Format";
    protected static final String RESPONSE_TAG_RUNNING_TIME = "RunningTime";
    protected static final String RESPONSE_TAG_RELEASEDATE = "ReleaseDate";
    protected static final String RESPONSE_TAG_THEATRICALDEBUT = "TheatricalReleaseDate";
    protected static final String RESPONSE_TAG_TITLE = "Title";
    protected static final String RESPONSE_TAG_OFFERSUMMARY = "OfferSummary";
    protected static final String RESPONSE_TAG_EDITORIALREVIEW = "EditorialReview";
    protected static final String RESPONSE_TAG_SOURCE = "Source";
    protected static final String RESPONSE_TAG_CONTENT = "Content";
    protected static final String RESPONSE_TAG_IMAGESET = "ImageSet";
    // protected static final String RESPONSE_TAG_SWATCHIMAGE = "SwatchImage";
    // protected static final String RESPONSE_TAG_SMALLIMAGE = "SmallImage";
    protected static final String RESPONSE_TAG_THUMBNAILIMAGE = "ThumbnailImage";
    protected static final String RESPONSE_TAG_TINYIMAGE = "TinyImage";
    protected static final String RESPONSE_TAG_MEDIUMIMAGE = "MediumImage";
    protected static final String RESPONSE_TAG_LARGEIMAGE = "LargeImage";
    protected static final String RESPONSE_TAG_URL = "URL";
    protected static final String RESPONSE_TAG_ITEM = "Item";

    protected static final String RESPONSE_ATTR_CATEGORY = "Category";
    protected static final String RESPONSE_VALUE_CATEGORY_PRIMARY = "primary";
    protected static final String RESPONSE_VALUE_CATEGORY_VARIANT = "variant";

    protected static final String RESPONSE_TAG_DEPARTMENT = "Department";
    protected static final String RESPONSE_TAG_AUDIENCE = "AudienceRating";
    protected static final String RESPONSE_TAG_BINDING = "Binding";
    protected static final String RESPONSE_TAG_DEWEY = "DeweyDecimalNumber";
    protected static final String RESPONSE_TAG_EDITION = "Edition";
    protected static final String RESPONSE_TAG_LANGUAGE = "Language";
    protected static final String RESPONSE_TAG_NAME = "Name";
    protected static final String RESPONSE_TAG_TYPE = "Type";
    protected static final String RESPONSE_TAG_LISTPRICE = "ListPrice";
    protected static final String RESPONSE_TAG_FORMATTEDPRICE = "FormattedPrice";
    protected static final String RESPONSE_TAG_LOWESTUSEDPRICE = "LowestUsedPrice";

    protected static final String VALUE_SEARCHINDEX_BOOKS = "Books";
    protected static final String RESPONSE_TAG_AUTHOR = "Author";
    protected static final String RESPONSE_TAG_CREATOR = "Creator";

    protected static final String RESPONSE_TAG_NUMBEROFPAGES = "NumberOfPages";
    protected static final String RESPONSE_TAG_PUBLICATIONDATE = "PublicationDate";

    protected static final String VALUE_SEARCHINDEX_GADGETS = "Electronics";

    protected static final String VALUE_SEARCHINDEX_MOVIES = "DVD";
    protected static final String RESPONSE_TAG_DIRECTOR = "Director";
    protected static final String RESPONSE_TAG_ACTOR = "Actor";
    protected static final String RESPONSE_TAG_RATIO = "AspectRatio";

    protected static final String VALUE_SEARCHINDEX_MUSIC = "Music";
    protected static final String RESPONSE_TAG_DISC = "Disc";
    protected static final String RESPONSE_TAG_TRACK = "Track";

    protected static final String RESPONSE_TAG_ARTIST = "Artist";
    protected static final String RESPONSE_TAG_ORIGINALRELEASEDATE = "OriginalReleaseDate";

    protected static final String VALUE_SEARCHINDEX_SOFTWARE = "Software";
    protected static final String RESPONSE_TAG_PLATFORM = "Platform";

    protected static final String VALUE_SEARCHINDEX_TOOLS = "Tools";

    protected static final String VALUE_SEARCHINDEX_TOYS = "Toys";

    protected static final String VALUE_SEARCHINDEX_VIDEOGAMES = "VideoGames";

    protected static final String RESPONSE_TAG_ESRBAGERATING = "ESRBAgeRating";
    protected static final String RESPONSE_TAG_FEATURES = "Features";
    protected static final String RESPONSE_TAG_SPECIALFEATURES = "SpecialFeatures";
    protected static final String RESPONSE_TAG_GENRE = "Genre";
    protected static final String RESPONSE_TAG_HARDWAREPLATFORM = "HardwarePlatform";

    /*
     * FILL THIS SET OUT WITH YOUR OWN SERVER CONTENT
     */
    private static final String SERVER_SCRIPT = BuildConfig.SERVER_SCRIPT; // YOU ARE SUPPOSED TO FILL THIS OUT!
    protected static final String DETAIL_START = BuildConfig.DETAIL_START; // YOU ARE SUPPOSED TO FILL THIS OUT!
    public static final String IMAGE_START = BuildConfig.IMAGE_START; // YOU ARE SUPPOSED TO FILL THIS OUT!
    protected static final String API_REST_HOST = BuildConfig.API_REST_HOST; // YOU ARE SUPPOSED TO FILL THIS OUT!
    protected static final String API_REST_URL = BuildConfig.API_REST_URL; // YOU ARE SUPPOSED TO FILL THIS OUT!
    protected static final String API_ITEM_LOOKUP = BuildConfig.API_ITEM_LOOKUP; // YOU ARE SUPPOSED TO FILL THIS OUT!
    protected static final String API_ITEM_SEARCH = BuildConfig.API_ITEM_SEARCH; // YOU ARE SUPPOSED TO FILL THIS OUT!
    protected static final String PARAM_API_KEY = BuildConfig.PARAM_API_KEY; // YOU ARE SUPPOSED TO FILL THIS OUT!
    protected static final String PARAM_API_VERSION = BuildConfig.PARAM_API_VERSION; // YOU ARE SUPPOSED TO FILL THIS OUT!
    protected static final String PARAM_API_SERVICE = BuildConfig.PARAM_API_SERVICE; // YOU ARE SUPPOSED TO FILL THIS OUT!
    protected static final String PARAM_OPERATION = BuildConfig.PARAM_OPERATION; // YOU ARE SUPPOSED TO FILL THIS OUT!
    protected static final String PARAM_IDTYPE = BuildConfig.PARAM_IDTYPE; // YOU ARE SUPPOSED TO FILL THIS OUT!
    protected static final String PARAM_SEARCHINDEX = BuildConfig.PARAM_SEARCHINDEX; // YOU ARE SUPPOSED TO FILL THIS OUT!
    protected static final String PARAM_ITEMID = BuildConfig.PARAM_ITEMID; // YOU ARE SUPPOSED TO FILL THIS OUT!
    protected static final String PARAM_RESPONSEGROUP = BuildConfig.PARAM_RESPONSEGROUP; // YOU ARE SUPPOSED TO FILL THIS OUT!
    protected static final String PARAM_KEYWORDS = BuildConfig.PARAM_KEYWORDS; // YOU ARE SUPPOSED TO FILL THIS OUT!
    protected static final String PARAM_ASSOCIATETAG = BuildConfig.PARAM_ASSOCIATETAG; // YOU ARE SUPPOSED TO FILL THIS OUT!
    protected static final String PARAM_ITEMPAGE = BuildConfig.PARAM_ITEMPAGE; // YOU ARE SUPPOSED TO FILL THIS OUT!

    protected static final String VALUE_SERVICE = BuildConfig.VALUE_SERVICE; // YOU ARE SUPPOSED TO FILL THIS OUT!
    protected static final String VALUE_VERSION = BuildConfig.VALUE_VERSION; // YOU ARE SUPPOSED TO FILL THIS OUT!
    protected static final String VALUE_ASSOCIATEID = BuildConfig.VALUE_ASSOCIATEID; // YOU ARE SUPPOSED TO FILL THIS OUT!
    public static final String API_REST_HOST_US = BuildConfig.API_REST_HOST_US; // YOU ARE SUPPOSED TO FILL THIS OUT!
    public static final String API_REST_HOST_CA = BuildConfig.API_REST_HOST_CA; // YOU ARE SUPPOSED TO FILL THIS OUT!
    public static final String API_REST_HOST_UK = BuildConfig.API_REST_HOST_UK; // YOU ARE SUPPOSED TO FILL THIS OUT!
    public static final String API_REST_HOST_FR = BuildConfig.API_REST_HOST_FR; // YOU ARE SUPPOSED TO FILL THIS OUT!
    public static final String API_REST_HOST_DE = BuildConfig.API_REST_HOST_DE; // YOU ARE SUPPOSED TO FILL THIS OUT!
    public static final String API_REST_HOST_JP = BuildConfig.API_REST_HOST_JP; // YOU ARE SUPPOSED TO FILL THIS OUT!
    public static final String API_REST_HOST_IT = BuildConfig.API_REST_HOST_IT; // YOU ARE SUPPOSED TO FILL THIS OUT!
    public static final String API_REST_HOST_CN = BuildConfig.API_REST_HOST_CN; // YOU ARE SUPPOSED TO FILL THIS OUT!

    // YOU ARE SUPPOSED TO FILL THESE OUT!
    private static final String[] API_KEYS = { BuildConfig.API_KEYS_ONE, BuildConfig.API_KEYS_TWO };

    // Dropbox stuff
    public final static String DROPBOX_APP_KEY = BuildConfig.DROPBOX_APP_KEY; // YOU ARE SUPPOSED TO FILL THIS OUT!
    public final static String DROPBOX_APP_SECRET = BuildConfig.DROPBOX_APP_SECRET; // YOU ARE SUPPOSED TO FILL THIS OUT!
    /*
     * END FILL; HAVE A NICE DAY!
     */

    private static final String LOG_TAG = "PublicServerInfo";

    // GJT: Fields common to all derivatives
    final protected String mHost = API_REST_HOST;
    protected ServerImageLoader mLoader = new ServerImageLoader();

    protected ServerInfo() {
    }

    public static class ServerImageLoader {
        public ImageUtilities.ExpiringBitmap load(String url) {
            final String cookie = CookieStore.get().getCookie(url);
            return ImageUtilities.load(url, cookie);
        }
    }

    protected static Uri.Builder assembleURI(String id, IOUtilities.inputTypes type, Context context,
            String searchIndex, String altTag) {

        if (type == null) {
            if (searchIndex.equals(VALUE_SEARCHINDEX_MUSIC))
                return buildFindQuery(id, searchIndex, context, altTag, VALUE_RESPONSEGROUP_LARGE);
            else
                return buildFindQuery(id, searchIndex, context, altTag);
        }

        switch (type) {
        case DLApparel:
        case DLBooks:
        case DLGadgets:
        case DLMovies:
        case DLMusic:
        case DLSoftware:
        case DLTools:
        case DLToys:
        case DLVideoGames:
            if (searchIndex.equals(VALUE_SEARCHINDEX_MUSIC))
                return buildFindQuery(id, searchIndex, context, RESPONSE_TAG_ASIN, VALUE_RESPONSEGROUP_LARGE);
            else
                return buildFindQuery(id, searchIndex, context, RESPONSE_TAG_ASIN);
        default:
            if (searchIndex.equals(VALUE_SEARCHINDEX_MUSIC))
                return buildFindQuery(id, searchIndex, context, altTag, VALUE_RESPONSEGROUP_LARGE);
            else
                return buildFindQuery(id, searchIndex, context, altTag);
        }

    }

    /**
     * Builds an HTTP GET request for the specified API method. The returned
     * request contains the web service path, the query parameter for the API
     * KEY and the query parameter for the specified method.
     * 
     * @param method
     *            The API method to invoke.
     * 
     * @return A Uri.Builder containing the GET path, the API key and the method
     *         already encoded.
     */
    protected static Uri.Builder buildGetMethod(String method, Context context) {
        final Uri.Builder builder = new Uri.Builder();

        builder.path(SERVER_SCRIPT);
        builder.appendQueryParameter("EndpointUri", SettingsActivity.getDatabaseRegion(context) + API_REST_URL);
        builder.appendQueryParameter(PARAM_API_KEY, randomAPIKey());
        builder.appendQueryParameter(PARAM_API_VERSION, VALUE_VERSION);
        builder.appendQueryParameter(PARAM_API_SERVICE, VALUE_SERVICE);
        builder.appendQueryParameter(PARAM_OPERATION, method);
        builder.appendQueryParameter(PARAM_ASSOCIATETAG, VALUE_ASSOCIATEID);

        return builder;
    }

    private static String randomAPIKey() {
        int randomPos = 0 + (int) (Math.random() * ((API_KEYS.length - 1 - 0) + 1));

        return API_KEYS[randomPos];
    }

    public static class Description {
        private String mSource;
        private String mContent;

        public Description(String source, String content) {
            Entities e = new Entities();
            mSource = source;

            // GJT: All this crap just because Google Books has some weird ASCII
            // response
            mContent = e.unescape(content.replaceAll("\\<.*?>", ""));
        }

        String getSource() {
            return mSource;
        }

        String getContent() {
            return mContent;
        }

        @Override
        public String toString() {
            // GJT: Changed this
            // TODO: We should be storing reviews in a separate table
            return mContent;
            /*
             * return "<p class=\".source\">" + mSource +
             * "</p>\n<p class=\".content\">" + mContent + "</p>";
             */
        }
    }

    /**
     * Constructs the query used to search for an item. 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 item.
     * 
     * @return The Uri to the list of item matching the query.
     */

    public static Uri.Builder buildSearchQuery(String query, String searchIndex, String page, Context context) {
        final Uri.Builder uri = buildGetMethod(API_ITEM_SEARCH, context);
        uri.appendQueryParameter(PARAM_KEYWORDS, query);
        uri.appendQueryParameter(PARAM_SEARCHINDEX, searchIndex);
        uri.appendQueryParameter(PARAM_RESPONSEGROUP, VALUE_RESPONSEGROUP_MEDIUM);
        uri.appendQueryParameter(PARAM_ITEMPAGE, page);
        // android.util.Log.d(LOG_TAG, uri.toString());
        return uri;
    }

    /**
     * Constructs the query used to find an item identified by its id. The
     * unique identifier should be either the EAN (ISBN-13) or ISBN (ISBN-10) of
     * the item to find.
     * 
     * @param id
     *            The EAN or ISBN of the item to find.
     * 
     * @return The Uri to the item details for this store.
     */

    public static Uri.Builder buildFindQuery(String id, String searchIndex, Context context, String responseType) {
        return buildFindQuery(id, searchIndex, context, responseType, VALUE_RESPONSEGROUP_MEDIUM);
    }

    public static Uri.Builder buildFindQuery(String id, String searchIndex, Context context, String responseType,
            String responseGroup) {

        /*
         * GJT: Clicking on an item in the Add*Activity list view sometimes
         * returns multiple results, like with EANs 025195018524 & 0786936786866
         * for DVDs. So if we're coming from an Add screen, override whatever
         * load[Item] says and force-use a search on the ASIN Otherwise, resume
         * as planned, like on imports or barcode lookups, which use
         * EAN/ISBN/UPC
         */
        Pattern pattern = Pattern.compile("Add[A-Za-z]+Activity");
        Matcher matcher = pattern.matcher(context.toString());

        final Uri.Builder uri = buildGetMethod(API_ITEM_LOOKUP, context);
        if (matcher.find() || responseType.equals(RESPONSE_TAG_ASIN)) {
            uri.appendQueryParameter(PARAM_IDTYPE, RESPONSE_TAG_ASIN);
            // GJT: Can't have a SearchIndex AND use ASIN
        } else {
            uri.appendQueryParameter(PARAM_SEARCHINDEX, searchIndex);
            uri.appendQueryParameter(PARAM_IDTYPE, responseType);
        }
        uri.appendQueryParameter(PARAM_RESPONSEGROUP, responseGroup);
        uri.appendQueryParameter(PARAM_ITEMID, id);
        return uri;
    }

    /**
     * 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
     */
    protected void executeRequest(HttpHost host, HttpGet get, ResponseHandler handler) throws IOException {

        HttpEntity entity = null;
        try {
            final HttpResponse response = HttpManager.execute(host, get);
            int statusCode = response.getStatusLine().getStatusCode();

            if (statusCode == HttpStatus.SC_MOVED_TEMPORARILY || statusCode == HttpStatus.SC_SEE_OTHER) {
                Header header = response.getFirstHeader("Location");
                if (header != null) {
                    String redirectURI = header.getValue();
                    if ((redirectURI != null) && (!redirectURI.equals(""))) {
                        android.util.Log.e(LOG_TAG, "Redirect target: " + redirectURI);
                        DefaultHttpClient httpclient = new DefaultHttpClient();
                        get = new HttpGet(redirectURI);
                        entity = httpclient.execute(get).getEntity();
                        final InputStream in = entity.getContent();
                        handler.handleResponse(in);
                    }
                } else {
                    android.util.Log.e(LOG_TAG, "Invalid redirect");
                }
            } else if (statusCode == HttpStatus.SC_OK) {
                entity = response.getEntity();
                final InputStream in = entity.getContent();
                handler.handleResponse(in);
            }
        } finally {
            if (entity != null) {
                entity.consumeContent();
            }
        }
    }

    /**
     * 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
     */
    public static void parseResponse(InputStream in, ResponseParser responseParser,
            IOUtilities.inputTypes inputType) throws IOException {
        final XmlPullParser parser = Xml.newPullParser();
        try {
            parser.setInput(new InputStreamReader(in));

            int type;
            while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) {
                // Empty
            }

            if (type != XmlPullParser.START_TAG) {
                throw new InflateException(parser.getPositionDescription() + ": No start tag found!");
            }

            String name;
            boolean valid = false;
            final int topDepth = parser.getDepth();

            while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > topDepth)
                    && type != XmlPullParser.END_DOCUMENT) {

                if (type != XmlPullParser.START_TAG) {
                    continue;
                }

                name = parser.getName();

                if (RESPONSE_TAG_REQUEST.equals(name)) {
                    valid = isRequestValid(parser);
                    break;
                }
            }

            if (valid)
                responseParser.parseResponse(parser);

        } catch (XmlPullParserException e) {
            final IOException ioe = new IOException("Could not parse the response");
            ioe.initCause(e);
            throw ioe;
        }
    }

    private static boolean isRequestValid(XmlPullParser parser) throws XmlPullParserException, IOException {

        int type;
        String name;
        boolean valid = false;
        final int depth = parser.getDepth();

        while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
                && type != XmlPullParser.END_DOCUMENT) {

            if (type != XmlPullParser.START_TAG) {
                continue;
            }

            name = parser.getName();

            if (RESPONSE_TAG_ISVALID.equals(name)) {
                if (parser.next() != XmlPullParser.TEXT || !"True".equals(parser.getText())) {
                    throw new IOException("Invalid request");
                } else {
                    valid = true;
                }
            } else if (RESPONSE_TAG_ERRORS.equals(name)) {
                valid = false;
            }
        }

        return valid;
    }

    /**
     * Finds the next item entry in the XML input stream.
     * 
     * @param parser
     *            The XML parser to use to parse the item.
     * 
     * @return True if an item was found, false otherwise.
     */
    protected boolean findNextItem(XmlPullParser parser) throws XmlPullParserException, IOException {
        if (RESPONSE_TAG_ITEM.equals(parser.getName())) {
            return true;
        }

        int type;
        final int depth = parser.getDepth();

        while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
                && type != XmlPullParser.END_DOCUMENT) {
            if (type != XmlPullParser.START_TAG) {
                continue;
            }

            if (RESPONSE_TAG_ITEM.equals(parser.getName())) {
                return true;
            }
        }

        return false;
    }

    /**
     * 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.
     */
    public 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.
     */
    public 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;
    }

    protected Description parseEditorialReview(XmlPullParser parser) throws IOException, XmlPullParserException {

        int type;
        String name;
        String source = null;
        String content = null;
        final int depth = parser.getDepth();

        while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
                && type != XmlPullParser.END_DOCUMENT) {
            if (type != XmlPullParser.START_TAG) {
                continue;
            }

            name = parser.getName();
            if (RESPONSE_TAG_SOURCE.equals(name)) {
                if (parser.next() == XmlPullParser.TEXT) {
                    source = parser.getText();
                }
            } else if (RESPONSE_TAG_CONTENT.equals(name)) {
                if (parser.next() == XmlPullParser.TEXT) {
                    content = parser.getText();
                }
            }
        }

        if (content != null) {
            return new Description(source, content);
        }
        return new Description("", "");
    }

    protected String parseImage(XmlPullParser parser) throws IOException, XmlPullParserException {

        int type;
        String name;
        final int depth = parser.getDepth();

        while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
                && type != XmlPullParser.END_DOCUMENT) {
            if (type != XmlPullParser.START_TAG) {
                continue;
            }

            name = parser.getName();
            if (RESPONSE_TAG_URL.equals(name)) {
                if (parser.next() == XmlPullParser.TEXT) {
                    return parser.getText();
                }
            }
        }
        return null;
    }

    protected String parseLanguage(XmlPullParser parser) throws XmlPullParserException, IOException {
        String langName = null;
        String langType = null;

        while (true) {
            int eventType = parser.next();
            if (eventType == XmlPullParser.START_TAG) {
                String tag = parser.getName();
                if (RESPONSE_TAG_NAME.equals(tag)) {
                    parser.next();
                    langName = parser.getText();
                } else if (RESPONSE_TAG_TYPE.equals(tag)) {
                    parser.next();
                    langType = parser.getText();
                }
            } else if (langType != null && eventType == XmlPullParser.END_TAG) {
                break;
            }
        }

        return langName + " (" + langType + ")";
    }
}