All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.brettonw.bag.JsonParser Maven / Gradle / Ivy

package com.brettonw.bag;

// The JsonParser is loosely modeled after a JSON parser grammar from the site (http://www.json.org).
// The main difference is that we ignore differences between value types (all of them will be
// strings internally), and assume the input is a well formed string representation of a BagObject
// or BagArray in JSON-ish format

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;

class JsonParser extends Parser {
    private static final Logger log = LogManager.getLogger (JsonParser.class);

    JsonParser (String input) {
        super (input);
    }

    JsonParser (InputStream inputStream) throws IOException {
        super (inputStream);
    }

    JsonParser (File file) throws IOException {
        super (file);
    }

    @Override
    BagArray readBagArray () {
        //  :: [ ] | [  ]
        BagArray bagArray = new BagArray();
        return (expect('[') && readElements (bagArray) && require(']')) ? bagArray : null;
    }

    private boolean storeValue (BagArray bagArray) {
        // the goal here is to try to read a "value" from the input stream, and store it into the
        // BagArray. BagArrays can store null values, so we have a special handling case to make
        // sure we properly convert "null" string to null value - as distinguished from a failed
        // read, which returns null value to start.the method returns true if a valid value was
        // fetched from the stream (in which case it was added to the BagArray)
        Object value = readValue ();
        if (value != null) {
            // special case for "null"
            if ((value instanceof String) && (((String) value).equalsIgnoreCase ("null"))) {
                value = null;
            }
            bagArray.add (value);
            return true;
        }
        return false;
    }

    private boolean readElements (BagArray bagArray) {
        //  ::=  |  , 
        boolean result = true;
        if (storeValue (bagArray)) {
            while (expect (',')) {
                result = require (storeValue (bagArray), "Valid value");
            }
        }
        return result;
    }

    @Override
    BagObject readBagObject () {
        //  ::= { } | {  }
        BagObject bagObject = new BagObject();
        return (expect('{') && readMembers (bagObject) && require('}')) ? bagObject : null;
    }

    private boolean readMembers (BagObject bagObject) {
        //  ::=  |  , 
        boolean result = true;
        if (readPair (bagObject)) {
            while (expect (',')) {
                result = require (readPair (bagObject), "Valid pair");
            }
        }
        return result;
    }

    private boolean storeValue (BagObject bagObject, String key) {
        // the goal here is to try to read a "value" from the input stream, and store it into the
        // BagObject. BagObject can NOT store null values, so we have a special handling case to
        // make sure we properly convert "null" string to null value - as distinguished from a failed
        // read, which returns null value to start. the method returns true if a valid value was
        // fetched from the stream, regardless of whether a null value was stored in the BagObject.
        Object value = readValue ();
        if (value != null) {
            // special case for "null"
            if (!((value instanceof String) && (((String) value).equalsIgnoreCase ("null")))) {
                bagObject.put (key, value);
            }
            return true;
        }
        return false;
    }

    private boolean readPair (BagObject bagObject) {
        //  ::=  : 
        String key = readString ();
        if ((key != null) && (key.length () > 0) && require (':')) {
            return require (storeValue (bagObject, key), "Valid value");
        }
        return false;
    }

    private static final char bareValueStopChars[] = sortString (" \t\n:{}[]\",");
    private static final char quotedStringStopChars[] = sortString ("\n\"");

    private static char[] sortString (String string) {
        char chars[] = string.toCharArray ();
        Arrays.sort (chars);
        return chars;
    }

    private boolean notIn (char stopChars[], char c) {
        int i = 0;
        int end = stopChars.length;
        char stopChar = 0;
        while ((i < end) && (c > (stopChar = stopChars[i]))) {
            ++i;
        }
        return stopChar != c;
    }

    private int consumeUntilStop (char stopChars[]) {
        int start = index;
        char c;
        //while (check () && (Arrays.binarySearch (stopChars, c = input.charAt (index)) < 0)) {
        while (check () && notIn (stopChars, (c = input.charAt (index)))) {
            // using the escape mechanism is like a free pass for the next character, but we
            // don't do any transformation on the substring, just return it as written
            index += (c == '\\') ? 2 : 1;
        }
        return start;
    }

    private String readString () {
        // " chars " | 
        String result = null;
        if (expect('"')) {
            // digest the string, and be sure to eat the end quote
            int start = consumeUntilStop (quotedStringStopChars);
            result = input.substring (start, index++);
        } else {
            // technically, we're being sloppy allowing bare values where quoted strings are
            // expected, but it's part of the simplified structure we support. This allows us to
            // read valid JSON files without handling every single case.
            int start = consumeUntilStop (bareValueStopChars);;

            // capture the result if we actually consumed some characters
            if (index > start) {
                result = input.substring (start, index);
            }
        }
        return result;
    }

    private Object readValue () {
        //  ::=  |  | 
        consumeWhiteSpace ();

        Object value = null;
        if (check ()) {
            switch (input.charAt (index)) {
                case '{':
                    value = readBagObject ();
                    break;

                case '[':
                    value = readBagArray ();
                    break;

                case '"':
                default:
                    value = readString ();
                    break;
            }
        }
        return value;
    }
}