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

Java tutorial

Introduction

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

Source

// @formatter:off
/*
 * ScheduleDeserializer.java - deserializer for TMA-1 schedule API
 * 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 static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import org.level28.android.moca.model.Session;

import android.text.TextUtils;
import android.text.format.Time;

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

/**
 * Deserializer for TMA-1 schedule API.
 * 
 * @author Matteo Panella
 */
public class ScheduleDeserializer extends AbstractJsonDeserializer<Map<String, Session>> {

    @Override
    public Map<String, Session> fromInputStream(InputStream in) throws JsonDeserializerException {
        JsonNode root;
        try {
            root = sJsonMapper.readTree(in);
        } catch (IOException e) {
            throw new JsonDeserializerException("Internal Jackson error", e);
        }

        if (!root.isArray()) {
            throw new JsonDeserializerException("Root node is not an array");
        }

        HashMap<String, Session> result = Maps.newHashMap();

        for (JsonNode node : root) {
            Session session = parseSession(node);
            result.put(session.getId(), session);
        }

        return result;
    }

    private static Session parseSession(final JsonNode objectRoot) throws JsonDeserializerException {
        // Basic sanity checks
        if (objectRoot == null || objectRoot.isNull()) {
            throw new JsonDeserializerException("null objectRoot");
        }
        if (!objectRoot.isObject()) {
            throw new JsonDeserializerException("objectRoot is not a JSON object");
        }

        JsonNode node;
        Session result = new Session();

        try {
            // Session id (required)
            node = objectRoot.path("id");
            if (node.isMissingNode() || !node.isTextual()) {
                throw new JsonDeserializerException("'id' is missing or invalid");
            }
            result.setId(node.textValue());

            // Session title (required)
            node = objectRoot.path("title");
            if (node.isMissingNode() || !node.isTextual()) {
                throw new JsonDeserializerException("'title' is missing or invalid");
            }
            result.setTitle(node.textValue());

            // Session day (required)
            node = objectRoot.path("day");
            if (node.isMissingNode() || !node.isInt()) {
                throw new JsonDeserializerException("'day' is missing or invalid");
            }
            result.setDay(node.asInt());

            // Session start time (required)
            node = objectRoot.path("start");
            if (node.isMissingNode() || !node.isTextual()) {
                throw new JsonDeserializerException("'start' is missing or invalid");
            }
            result.setStartTime(parseTime(node.textValue()));

            // Session end time (required)
            node = objectRoot.path("end");
            if (node.isMissingNode() || !node.isTextual()) {
                throw new JsonDeserializerException("'end' is missing or invalid");
            }
            result.setEndTime(parseTime(node.textValue()));

            // Session hosts (required)
            node = objectRoot.path("hosts");
            if (node.isMissingNode() || !node.isArray()) {
                throw new JsonDeserializerException("'hosts' is missing or invalid");
            }
            final ArrayList<String> hosts = new ArrayList<String>(node.size());
            for (JsonNode hostsSubNode : node) {
                if (!hostsSubNode.isTextual() || "".equals(hostsSubNode.textValue())) {
                    throw new JsonDeserializerException("'hosts' children is not valid");
                }
                hosts.add(hostsSubNode.textValue());
            }
            result.setHosts(TextUtils.join(", ", hosts));

            // Session language (required)
            node = objectRoot.path("lang");
            if (node.isMissingNode() || !node.isTextual()) {
                throw new JsonDeserializerException("'lang' is missing or invalid");
            }
            result.setLang(node.textValue());

            // Session abstract (optional)
            node = objectRoot.path("abstract");
            if (!node.isMissingNode()) {
                result.setSessionAbstract(node.textValue());
            }

            return result;
        } catch (IllegalArgumentException e) {
            throw new JsonDeserializerException("Invalid session entry", e);
        } catch (ParseException e) {
            throw new JsonDeserializerException("Invalid session entry", e);
        }
    }

    /**
     * Pure Java reimplementation of {@link Time#parse3339(String)}.
     * <p>
     * Due to <a
     * href="http://code.google.com/p/android/issues/detail?id=16002">Issue
     * 16002</a>, {@code Time.parse3339()} leaks memory on short input. However,
     * the same leak happens <em>also</em> if the function is fed a formally
     * invalid RFC3339 timestamp.
     * <p>
     * The safest option is a full rewrite in pure Java, since JNI on Android is
     * a mess (we'd have to ship the same library for three different
     * architectures &mdash; not pretty...).
     * 
     * @param timeString
     *            a date/time specification formatted as an RFC3339 string
     * @return the same date/time specification in milliseconds since the Epoch
     * @throws ParseException
     *             if the string is formally invalid per RFC3339
     * @throws NullPointerException
     *             if the string is {@code null}
     * @throws IllegalArgumentException
     *             if the string is less than 10 characters long
     */
    private static long parseTime(String timeString) throws ParseException {
        checkNotNull(timeString, "Time input should not be null");
        final int len = timeString.length();
        checkArgument(len >= 10, "Time input is too short; must be at least 10 characters");
        final char[] s = timeString.toCharArray();

        final Time t = new Time();
        int n;
        boolean inUtc = false;

        // Year
        n = getChar(s, 0, 1000);
        n += getChar(s, 1, 100);
        n += getChar(s, 2, 10);
        n += getChar(s, 3, 1);
        t.year = n;

        // '-'
        checkChar(s, 4, '-');

        // Month
        n = getChar(s, 5, 10);
        n += getChar(s, 6, 1);
        --n;
        t.month = n;

        // '-'
        checkChar(s, 7, '-');

        // Day
        n = getChar(s, 8, 10);
        n += getChar(s, 9, 1);
        t.monthDay = n;

        // Check if we have a time as well
        if (len >= 19) {
            // 'T'
            checkChar(s, 10, 'T');
            t.allDay = false;

            // Hour
            n = getChar(s, 11, 10);
            n += getChar(s, 12, 1);
            int hour = n;

            // ':'
            checkChar(s, 13, ':');

            // Minute
            n = getChar(s, 14, 10);
            n += getChar(s, 15, 1);
            int minute = n;

            // ':'
            checkChar(s, 16, ':');

            // Second
            n = getChar(s, 17, 10);
            n += getChar(s, 18, 1);
            t.second = n;

            // Skip subsecond component (if any)
            int tz_index = 19;
            if (tz_index < len && s[tz_index] == '.') {
                do {
                    tz_index++;
                } while (tz_index < len && s[tz_index] >= '0' && s[tz_index] <= '9');
            }

            int offset = 0;
            if (len > tz_index) {
                final char c = s[tz_index];

                switch (c) {
                case 'Z':
                    // UTC
                    offset = 0;
                    break;
                case '-':
                    offset = 1;
                    break;
                case '+':
                    offset = -1;
                    break;
                default:
                    throw new ParseException("Unexpected character", tz_index);
                }
                inUtc = true;

                if (offset != 0) {
                    // Hour
                    n = getChar(s, tz_index + 1, 10);
                    n += getChar(s, tz_index + 2, 1);
                    n *= offset;
                    hour += n;

                    // ':'
                    checkChar(s, tz_index + 3, ':');

                    // Minute
                    n = getChar(s, tz_index + 4, 10);
                    n += getChar(s, tz_index + 5, 1);
                    n *= offset;
                    minute += n;
                }
            }
            t.hour = hour;
            t.minute = minute;

            if (offset != 0) {
                t.normalize(false);
            }
        } else {
            // We don't
            t.allDay = true;
            t.hour = 0;
            t.minute = 0;
            t.second = 0;
        }

        t.weekDay = 0;
        t.yearDay = 0;
        t.isDst = -1;
        t.gmtoff = 0;

        if (inUtc) {
            t.timezone = Time.TIMEZONE_UTC;
        }

        return t.toMillis(false);
    }

    private static int getChar(final char[] s, int index, int multiplier) throws ParseException {
        final char c = s[index];
        if (c >= '0' && c <= '9') {
            return Character.digit(c, 10) * multiplier;
        }
        throw new ParseException("Illegal character", index);
    }

    private static void checkChar(final char[] s, int index, char expected) throws ParseException {
        if (s[index] != expected) {
            throw new ParseException("Unexpected character", index);
        }
    }
}