com.lillicoder.newsblurry.feeds.FeedParser.java Source code

Java tutorial

Introduction

Here is the source code for com.lillicoder.newsblurry.feeds.FeedParser.java

Source

/**
 * Copyright 2012 Scott Weeden-Moody
 *
 * 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.lillicoder.newsblurry.feeds;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;

import com.lillicoder.newsblurry.R;

/**
 * Utility class that can generate {@link Feed}.
 * @author lillicoder
 */
public class FeedParser {

    private static final String TAG = FeedParser.class.getSimpleName();

    private static final String JSON_KEY_FAVICON = "favicon";

    private static final String JSON_KEY_FEED_ID = "id";

    private static final String JSON_KEY_FEED_TITLE = "feed_title";

    private static final String JSON_KEY_FEEDS = "feeds";

    private static final String JSON_KEY_FOLDERS = "folders";

    private static final String JSON_KEY_UNREAD_LIKE_COUNT = "ps";

    private static final String JSON_KEY_UNREAD_NEUTRAL_COUNT = "nt";

    private static final String JSON_KEY_UNREAD_DISLIKE_COUNT = "ng";

    private static final String EXCEPTION_NULL_CONSTRUCTOR_CONTEXT = "The given context must not be null.";

    private static final String WARNING_FAILED_TO_DECODE_FAVICON = "Failed to decode favicon, defaulting to no favicon.";

    private static final String WARNING_FAILED_TO_PARSE_FOLDER_JSON = "Failed to parse folder JSON, JSON format is invalid (bad entries or no mappings).";

    /**
     * Default drawable resource for feed favicons.
     */
    private static final int DEFAULT_FEED_FAVICON_RESOURCE = R.drawable.ic_feed_favicon;

    /**
     * ID value for a child folder in a folder JSON node.
     */
    private static final int FOLDER_ID = -1;

    private Context _context;

    public FeedParser(Context context) {
        if (context == null)
            throw new IllegalArgumentException(EXCEPTION_NULL_CONSTRUCTOR_CONTEXT);

        this.setContext(context);
    }

    /**
     * Decodes the given base-64 favicon string into a {@link Favicon}.
     * @param encodedFavicon Base-64 encoded favicon to decode.
     * @return Decoded {@link Favicon},
     *          <code>null</code> if the encoded string could not be decoded.
     */
    private Favicon decodeFavicon(String encodedFavicon) {
        Favicon favicon = null;

        if (!TextUtils.isEmpty(encodedFavicon)) {
            try {
                byte[] rawFavicon = Base64.decode(encodedFavicon, Base64.DEFAULT);
                Bitmap faviconImage = BitmapFactory.decodeByteArray(rawFavicon, 0, rawFavicon.length);

                if (faviconImage != null)
                    favicon = new Favicon(faviconImage);
            } catch (IllegalArgumentException e) {
                Log.w(TAG, WARNING_FAILED_TO_DECODE_FAVICON, e);
            }
        }

        return favicon;
    }

    /**
     * Gets the default {@link Favicon} for feeds.
     * @return Default feed {@link Favicon}.
     */
    private Favicon getDefaultFeedFavicon() {
        Favicon defaultFavicon = null;

        Context context = this.getContext();
        Resources resources = context.getResources();
        Bitmap defaultFaviconImage = BitmapFactory.decodeResource(resources, DEFAULT_FEED_FAVICON_RESOURCE);

        if (defaultFaviconImage != null)
            defaultFavicon = new Favicon(defaultFaviconImage);

        return defaultFavicon;
    }

    /**
     * Parses the given feed {@link JSONObject} into a {@link Feed} instance.
     * @param feedJson Feed {@link JSONObject} to parse.
     * @return Parsed {@link Feed} instance.
     * @throws JSONException Thrown if expected feed value are not present in the given feed JSON. 
     */
    private Feed parseFeed(JSONObject feedJson) throws JSONException {
        int id = feedJson.getInt(JSON_KEY_FEED_ID);
        String title = feedJson.getString(JSON_KEY_FEED_TITLE);

        String encodedFavicon = feedJson.getString(JSON_KEY_FAVICON);
        Favicon favicon = this.decodeFavicon(encodedFavicon);
        if (favicon == null)
            // Favicon failed to parse, use default feed favicon.
            favicon = this.getDefaultFeedFavicon();

        int unreadDislike = feedJson.getInt(JSON_KEY_UNREAD_DISLIKE_COUNT);
        int unreadNeutral = feedJson.getInt(JSON_KEY_UNREAD_NEUTRAL_COUNT);
        int unreadLike = feedJson.getInt(JSON_KEY_UNREAD_LIKE_COUNT);
        UnreadCounts counts = new UnreadCounts(unreadDislike, unreadNeutral, unreadLike);

        return new Feed(id, title, favicon, counts);
    }

    /**
     * Parses the given feeds {@link JSONArray} into a map of feeds with each
     * entry keyed by feed ID.
     * @param feedsRootJson Root feeds {@link JSONObject} to parse.
     * @return {@link Map} of {@link Feed} keyed by feed ID.
     * @throws JSONException Thrown when the feeds JSON array cannot be parsed.
     */
    public Map<Integer, Feed> parseFeedsById(JSONArray feedsArrayJson) throws JSONException {
        Map<Integer, Feed> feedsById = new HashMap<Integer, Feed>();

        if (feedsArrayJson != null) {
            // Get each array entry and place the feed into the map.
            for (int index = 0; index < feedsArrayJson.length(); index++) {
                JSONObject feedJson = feedsArrayJson.getJSONObject(index);
                Feed feed = this.parseFeed(feedJson);

                feedsById.put(feed.getId(), feed);
            }
        }

        return feedsById;
    }

    /**
     * Parses the given feeds root {@link JSONObject} into a map of feeds
     * with each entry keyed by feed ID.
     * @param JSONObject feedsRootJson Feeds root {@link JSONObject}.
     * @return {@link Map} of {@link Feed} keyed by feed ID.
     * @throws JSONException Thrown when the feeds root JSON node cannot be parsed.
     */
    public Map<Integer, Feed> parseFeedsById(JSONObject feedsRootJson) throws JSONException {
        Map<Integer, Feed> feedsById = new HashMap<Integer, Feed>();

        // Convert feeds node into an array of elements
        // using the child node's names.
        JSONObject feedsJson = feedsRootJson.getJSONObject(JSON_KEY_FEEDS);
        JSONArray feedsArrayJson = feedsJson.toJSONArray(feedsJson.names());
        feedsById = parseFeedsById(feedsArrayJson);

        return feedsById;
    }

    /**
     * Parses the given root feeds {@link JSONObject} into a collection of
     * {@link IFeed} populated with all child feeds and folders.
     * Any given folder may contain both {@link FeedFolder} and {@link Feed}.
     * @param feedsRootJson Root feeds {@link JSONObject} to parse.
     * @return {@link List} of parsed {@link IFeed}.
     * @throws JSONException Thrown when given root feeds JSON node has an invalid format
     *                    or is missing feeds/folders data and cannot reliably be parsed.
     */
    public List<IFeed> parseFeeds(JSONObject feedsRootJson) throws JSONException {
        List<IFeed> rootFeeds = new ArrayList<IFeed>();

        // Parse folder structure.
        FeedCache cache = new FeedCache();
        JSONArray foldersArray = feedsRootJson.getJSONArray(JSON_KEY_FOLDERS);
        for (int index = 0; index < foldersArray.length(); index++) {
            try {
                Object objectJson = foldersArray.get(index);
                if (objectJson instanceof JSONObject) {
                    JSONObject folderJson = foldersArray.getJSONObject(index);
                    FeedFolder folder = this.parseFeedFolder(folderJson, null);
                    if (folder != null)
                        rootFeeds.add(folder);
                } else {
                    Integer feedId = (Integer) objectJson;
                    Feed feed = cache.getFeed(feedId);
                    if (feed != null)
                        rootFeeds.add(feed);
                }
            } catch (Exception e) {
                // Occurs when folder JSON has an invalid format of some kind, 
                // we should just skip this folder and move on.
                Log.w(TAG, WARNING_FAILED_TO_PARSE_FOLDER_JSON, e);
            }
        }

        return rootFeeds;
    }

    /**
     * Parses the given folder {@link JSONObject} into a {@link FeedFolder} instance.
     * If the given parent folder paramter is not null, that folder will be set as the
     * parsed folder's parent.
     * @param folderJson Folder {@link JSONObject} to parse.
     * @param parentFolder Optional parent {@link FeedFolder}. If this is not <code>null</code>,
     *                   the parsed folder will set its parent to this parameter.
     * @return Parsed {@link FeedFolder}.
     * @throws JSONException Thrown when the folder JSON has an invalid format and cannot be parsed.
     * @throws NullPointerException Thrown when the folder JSON has no mappings.
     */
    private FeedFolder parseFeedFolder(JSONObject folderJson, FeedFolder parentFolder)
            throws JSONException, NullPointerException {
        // Pull the given JSON node's folder name from the
        // names array.
        JSONArray folderNameJson = folderJson.names();
        String folderName = folderNameJson.getString(0);

        // Generate folder instance with retrieved title.
        FeedFolder folder;
        if (parentFolder != null)
            folder = new FeedFolder(folderName, parentFolder);
        else
            folder = new FeedFolder(folderName);

        // Get folder contents and iterate.
        FeedCache cache = new FeedCache();
        JSONArray folderContents = folderJson.getJSONArray(folderName);
        for (int index = 0; index < folderContents.length(); index++) {
            int feedId = folderContents.optInt(index, FOLDER_ID);
            if (feedId == FOLDER_ID) {
                // Child folder, parse folder.
                JSONObject childFolderJson = folderContents.getJSONObject(index);
                FeedFolder childFolder = this.parseFeedFolder(childFolderJson, folder);
                if (childFolder != null)
                    folder.addChildFolder(childFolder);
            } else {
                // Normal feed, set ID.
                Feed feed = cache.getFeed(feedId);
                if (feed != null)
                    folder.addChildFeed(feed);
            }
        }

        return folder;
    }

    /**
     * Gets the {@link Context} for this parser.
     * @return Parser {@link Context}.
     */
    private Context getContext() {
        return this._context;
    }

    /**
     * Sets the {@link Context} for this parser.
     * @param context {@link Context} to set.
     */
    private void setContext(Context context) {
        this._context = context;
    }

}