com.googlecode.fascinator.redbox.plugins.curation.external.FormDataParser.java Source code

Java tutorial

Introduction

Here is the source code for com.googlecode.fascinator.redbox.plugins.curation.external.FormDataParser.java

Source

/*
 * The Fascinator - Form Data Parser
 * Copyright (C) 2011 Queensland Cyber Infrastructure Foundation (http://www.qcif.edu.au/)
 *
 * 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.
 */
package com.googlecode.fascinator.redbox.plugins.curation.external;

import com.googlecode.fascinator.common.JsonObject;
import com.googlecode.fascinator.common.JsonSimple;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;

import org.json.simple.JSONArray;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The purpose of this class is to parse ReDBox form data packages and break the
 * the complicated String field names into meaningful JSON data structures that
 * are easier to traverse and interrogate.
 *
 * @author Greg Pendlebury
 */
public class FormDataParser {
    /** Logging **/
    @SuppressWarnings("unused")
    private static Logger log = LoggerFactory.getLogger(FormDataParser.class);

    /** Excluded top level nodes from parse */
    private static final List<String> EXCLUDED_FIELDS = Arrays.asList("metaList", "relationships", "responses");

    /**
     * A wrapper for Stream based parsing. This method accepts a String and
     * will internally create a Stream for it.
     *
     * @param input The form data to parse from a String
     * @return JsonSimple The parsed form data in JSON
     * @throws IOException if there are errors reading/parsing the form data
     */
    public static JsonSimple parse(String input) throws IOException {
        ByteArrayInputStream bytes = new ByteArrayInputStream(input.getBytes("UTF-8"));
        return parse(bytes);
    }

    /**
     * Accept and parse raw JSON data from an InputStream. Field name String
     * literals will be broken down into meaningful JSON data structures.
     *
     * @param input The form data to parse from an InputStream
     * @return JsonSimple The parsed form data in JSON
     * @throws IOException if there are errors reading/parsing the form data
     */
    public static JsonSimple parse(InputStream input) throws IOException {
        JsonSimple inputData = new JsonSimple(input);
        JsonSimple responseData = new JsonSimple();

        // Go through every top level node
        JsonObject object = inputData.getJsonObject();
        for (Object key : object.keySet()) {
            // Ignoring some non-form related nodes
            String strKey = validString(key);
            if (!EXCLUDED_FIELDS.contains(strKey)) {
                // And parse them into the repsonse
                String data = validString(object.get(key));
                parseField(responseData, strKey, data);
            }
        }
        return responseData;
    }

    /**
     * Ensures one of the generic objects coming from the JSON library
     * is in fact a String.
     *
     * @param data The generic object
     * @return String The String instance of the object
     * @throws IOException if the object is not a String
     */
    private static String validString(Object data) throws IOException {
        // Null is ok
        if (data == null) {
            return "";
        }
        if (!(data instanceof String)) {
            throw new IOException("Invalid non-String value found!");
        }
        return (String) data;
    }

    /**
     * Parse an individual field into the response object.
     *
     * @param response The response JSON data structure being built.
     * @param field The current field name to parse
     * @param data The data contained in this current field
     * @throws IOException if errors occur during the parse
     */
    private static void parseField(JsonSimple response, String field, String data) throws IOException {
        // Break it into pieces
        String[] fieldParts = field.split("\\.");
        // These are used as replacable pointers
        //  to the last object used on the path
        JsonObject lastObject = null;
        JSONArray lastArray = null;

        for (int i = 0; i < fieldParts.length; i++) {
            // What is this segment?
            String segment = fieldParts[i];
            int number = parseInt(segment);

            //**************************
            // 1 (of 3) The first segment adds our
            //    new empty object to the repsonse
            if (i == 0) {
                JsonObject topObject = response.getJsonObject();
                // Numbers aren't allowed here
                if (number != -1) {
                    throw new IOException(
                            "Field '" + field + "' starts with" + " an array... this is illegal form data!");
                }

                // Really simple fields... just one segment
                if (i + 1 == fieldParts.length) {
                    topObject.put(segment, data);

                    // Longer field, but what to add?
                } else {
                    String nextSegment = fieldParts[i + 1];
                    int nextNumber = parseInt(nextSegment);

                    // Objects... nextSegment is a String key
                    if (nextNumber == -1) {
                        lastObject = getObject(topObject, segment);
                        lastArray = null;

                        // Arrays... nextSegment is an integer index 
                    } else {
                        lastObject = null;
                        lastArray = getArray(topObject, segment);
                    }
                }

            } else {
                //**************************
                // 2 (of 3) The last segment is pretty simple
                if (i == (fieldParts.length - 1)) {
                    lastObject.put(segment, data);

                    //**************************
                    // 3 (of 3) Anything in between
                } else {
                    // Check what comes next
                    String nextSegment = fieldParts[i + 1];
                    int nextNumber = parseInt(nextSegment);

                    // We are populating an object
                    if (lastArray == null) {
                        // So we shouldn't be looking at a number
                        if (number != -1) {
                            // In theory you'd need a logic bug to reach here;
                            // illegal syntax should have been caught already.
                            throw new IOException("Field '" + field + "' has an" + " illegal syntax!");
                        }

                        // Objects... nextSegment is a String key
                        if (nextNumber == -1) {
                            lastObject = getObject(lastObject, segment);
                            lastArray = null;

                            // Arrays... nextSegment is an integer index 
                        } else {
                            lastArray = getArray(lastObject, segment);
                            lastObject = null;
                        }

                        // Populating an array
                    } else {
                        // We should be looking at number
                        if (number == -1) {
                            // In theory you'd need a logic bug to reach here;
                            // illegal syntax should have been caught already.
                            throw new IOException("Field '" + field + "' has an" + " illegal syntax!");
                        }

                        // This is actually quite simple, because we can only
                        //  store objects. 
                        lastObject = getObject(lastArray, number);
                        lastArray = null;
                    }
                }
            }
        }
    }

    /**
     * Get a child JSON Object from an incoming JSON array. If the child does
     * not exist it will be created, along with any smaller index values. The
     * index is expected to be 1 based (like form data).
     * 
     * It is only valid for form arrays to hold JSON Objects.
     * 
     * @param array The incoming array we are to look inside
     * @param index The child index we are looking for (1 based)
     * @return JsonObject The child we found or created
     * @throws IOException if anything other than an object is found, or an
     * invalid index is provided
     */
    private static JsonObject getObject(JSONArray array, int index) throws IOException {
        // We can't just jam an entry into the array without
        //  checking that earlier indexes exist. Also we need
        //  to account for 0 versus 1 based indexing.

        // Index changed to 0 based
        if (index < 1) {
            throw new IOException("Invalid index value provided in form data.");
        }
        index -= 1;

        // Nice and easy, it already exists
        if (array.size() > index) {
            Object object = array.get(index);
            if (object instanceof JsonObject) {
                return (JsonObject) object;
            }
            throw new IOException("Non-Object found in array!");

            // Slightly more annoying, we need to fill in
            //  all the indices up to this point
        } else {
            for (int i = array.size(); i <= index; i++) {
                JsonObject object = new JsonObject();
                array.add(object);
            }
            return (JsonObject) array.get(index);
        }
    }

    /**
     * Get a child JSON Array from an incoming JSON object. If the child does
     * not exist it will be created.
     *
     * @param object The incoming object we are to look inside
     * @param key The child node we are looking for
     * @return JSONArray The child we found or created
     * @throws IOException if there is a type mismatch on existing data
     */
    private static JSONArray getArray(JsonObject object, String key) throws IOException {
        // Get the existing one
        if (object.containsKey(key)) {
            Object existing = object.get(key);
            if (!(existing instanceof JSONArray)) {
                throw new IOException("Invalid field structure, '" + key
                        + "' expected to be an array, but incompatible " + "data type already present.");
            }
            return (JSONArray) existing;

            // Or add a new one
        } else {
            JSONArray newObject = new JSONArray();
            object.put(key, newObject);
            return newObject;
        }
    }

    /**
     * Get a child JSON Object from an incoming JSON object. If the child does
     * not exist it will be created.
     *
     * @param object The incoming object we are to look inside
     * @param key The child node we are looking for
     * @return JsonObject The child we found or created
     * @throws IOException if there is a type mismatch on existing data
     */
    private static JsonObject getObject(JsonObject object, String key) throws IOException {
        // Get the existing one
        if (object.containsKey(key)) {
            Object existing = object.get(key);
            if (!(existing instanceof JsonObject)) {
                throw new IOException("Invalid field structure, '" + key
                        + "' expected to be an object, but incompatible " + "data type already present.");
            }
            return (JsonObject) existing;

            // Or add a new one
        } else {
            JsonObject newObject = new JsonObject();
            object.put(key, newObject);
            return newObject;
        }
    }

    /**
     * Parse a String to an integer. This wrapper is simply to avoid the try
     * catch statement repeatedly. Tests for -1 are sufficient, since it is
     * illegal in form data. Valid integers below 1 throw exceptions because
     * of this illegality.
     *
     * @param integer The incoming integer to parse
     * @return int The parsed integer, or -1 if it is not an integer
     * @throws IOException if errors occur during the parse
     */
    private static int parseInt(String integer) throws IOException {
        try {
            int value = Integer.parseInt(integer);
            if (value < 0) {
                throw new IOException("Invalid number in field name: '" + integer + "'");
            }
            return value;
        } catch (NumberFormatException ex) {
            // It's not an integer
            return -1;
        }
    }
}