org.level28.android.moca.json.TwitterSearchDeserializer.java Source code

Java tutorial

Introduction

Here is the source code for org.level28.android.moca.json.TwitterSearchDeserializer.java

Source

// @formatter:off
/*
 * TwitterSearchDeserializer.java - JSON deserializer for Twitter search results
 * Copyright (C) 2012 Matteo Panella <morpheus@level28.org>
 *
 * This program 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 2
 * of the License, or (at your option) any later version.
 *
 * This program 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 this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
// @formatter:on

package org.level28.android.moca.json;

import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Locale;

import org.level28.android.moca.model.Tweet;
import org.level28.android.moca.model.TwitterSearchReply;

import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.collect.Lists;

/**
 * JSON deserializer for Twitter search results.
 * 
 * @author Matteo Panella
 */
public final class TwitterSearchDeserializer extends AbstractJsonDeserializer<TwitterSearchReply> {
    /** Date format used by Twitter */
    private static final String DATE_FORMAT = "E, dd MMM yyyy HH:mm:ss Z";

    /** Date format used in Twitter Search API replies */
    private static final SimpleDateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT, Locale.US);

    /**
     * Deserialize a JSON search result from the given {@link InputStream}.
     * 
     * @param in
     *            the input stream from which the JSON document should be parsed
     * @return a list of results with relevant search metadata
     * @throws JsonDeserializerException
     *             if the JSON document is invalid
     */
    @Override
    public TwitterSearchReply fromInputStream(InputStream in) throws JsonDeserializerException {
        JsonNode root;
        try {
            root = sJsonMapper.readTree(in);
        } catch (IOException e) {
            throw new JsonDeserializerException("Internal Jackson error: " + e.getMessage(), e);
        }

        // Root should be an object
        if (!root.isObject()) {
            throw new JsonDeserializerException("Root JsonNode is not an object!");
        }

        JsonNode node, resultsArray;
        TwitterSearchReply reply = new TwitterSearchReply();

        // Get all required attributes starting with the all-important results
        // array
        resultsArray = root.path("results");
        if (resultsArray.isMissingNode() || !resultsArray.isArray()) {
            throw new JsonDeserializerException("Invalid search reply (results missing or not an array)");
        }
        // We'll get to results deserialization later, continue to hunt down
        // required search reply fields

        // Original query (not really needed, just checked for sanity reasons)
        node = root.path("query");
        if (node.isMissingNode() || !node.isTextual()) {
            throw new JsonDeserializerException("'query' missing or invalid");
        }
        reply.setQuery(node.textValue());

        // Refresh url for this query
        node = root.path("refresh_url");
        if (node.isMissingNode() || !node.isTextual()) {
            throw new JsonDeserializerException("'refresh_url' missing or invalid");
        }
        reply.setRefreshUrl(node.textValue());

        // Maximum tweet id
        node = root.path("max_id");
        if (node.isMissingNode() || !node.canConvertToLong()) {
            throw new JsonDeserializerException("'max_id' missing or invalid");
        }
        reply.setMaxId(node.asLong());

        // Starting tweet id
        node = root.path("since_id");
        if (node.isMissingNode() || !node.canConvertToLong()) {
            throw new JsonDeserializerException("'since_id' missing or invalid");
        }
        reply.setSinceId(node.asLong());

        // Results per page
        node = root.path("results_per_page");
        if (node.isMissingNode() || !node.isInt()) {
            throw new JsonDeserializerException("'results_per_page' missing or invalid");
        }
        reply.setResultsPerPage(node.intValue());

        // Current page
        node = root.path("page");
        if (node.isMissingNode() || !node.isInt()) {
            throw new JsonDeserializerException("'page' missing or invalid");
        }
        reply.setPage(node.intValue());

        // Parse all results
        ArrayList<Tweet> tweets = Lists.newArrayList();
        for (JsonNode child : resultsArray) {
            tweets.add(parseSingleTweet(child));
        }
        reply.setResults(tweets);

        // Go for optional and low-priority attributes
        reply.setCompletedIn(root.path("completed_in").asDouble(666.0));
        reply.setNextPage(root.path("next_page").textValue());

        // Aaaaand we're done :-)
        return reply;
    }

    /**
     * Parse a single tweet.
     */
    private static Tweet parseSingleTweet(JsonNode objectRoot) throws JsonDeserializerException {
        // Basic sanity check
        if (!objectRoot.isObject()) {
            throw new JsonDeserializerException("Tweet JsonNode is not an object");
        }
        JsonNode node;
        Tweet tweet = new Tweet();

        // When was this tweet created?
        node = objectRoot.path("created_at");
        if (node.isMissingNode() || !node.isTextual()) {
            throw new JsonDeserializerException("'created_at' missing or invalid");
        }
        try {
            tweet.setCreatedAt(mDateFormat.parse(node.textValue()));
        } catch (ParseException e) {
            throw new JsonDeserializerException("Invalid date specified in 'created_at'", e);
        }

        // Who sent it? (user handle)
        node = objectRoot.path("from_user");
        if (node.isMissingNode() || !node.isTextual()) {
            throw new JsonDeserializerException("'from_user' missing or invalid");
        }
        tweet.setFromUser(node.textValue());

        // Who sent it? (user id)
        node = objectRoot.path("from_user_id");
        if (node.isMissingNode() || !node.canConvertToLong()) {
            throw new JsonDeserializerException("'from_user_id' missing or invalid");
        }
        tweet.setFromUserId(node.asLong());

        // Tweet id
        node = objectRoot.path("id");
        if (node.isMissingNode() || !node.canConvertToLong()) {
            throw new JsonDeserializerException("'id' missing or invalid");
        }
        tweet.setId(node.asLong());

        // Profile image url - prefer https over http
        node = objectRoot.path("profile_image_url_https");
        if (node.isMissingNode() || !node.isTextual()) {
            // Fall back to http
            node = objectRoot.path("profile_image_url");
            if (node.isMissingNode() || !node.isTextual()) {
                throw new JsonDeserializerException("'profile_image_url' missing or invalid");
            }
        }
        tweet.setProfileImageUrl(node.textValue());

        // Finally: tweet contents!
        node = objectRoot.path("text");
        if (node.isMissingNode() || !node.isTextual()) {
            throw new JsonDeserializerException("'text' missing or invalid");
        }
        tweet.setText(node.textValue());

        // FIXME: entities

        // Optional fields
        // Who sent it? (display name - yes, apparently this is OPTIONAL!)
        tweet.setFromUserName(objectRoot.path("from_user_name").textValue());

        // Free-form location
        tweet.setLocation(objectRoot.path("location").textValue());

        // FIXME: geo

        // End of tweet :-)
        return tweet;
    }
}