Start line:  
End line:  

Snippet Preview

Snippet HTML Code

Stack Overflow Questions
  /*
   * Copyright (C) 2014 Dell, Inc.
   * 
   * 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.dell.doradus.common;
 
 import java.io.Reader;

JSON parser that uses the SAX-style interface for JSON, aka "SAJ". As constructs are recognized, appropriate methods in the JSONAnnie.SajListener object passed to parse(com.dell.doradus.common.JSONAnnie.SajListener) are called.

Note that this class supports only the subset of JSON that Doradus uses. As a result, there are a few oddities, as described below:

  • Arrays within arrays are not supported. That is, foo: [10, [11, 12]] will throw an error when the inner array is seen.
  • All Doradus messages use the general form {"foo": {...stuff...}}. The outer braces are silently parsed and ignore. When the sequence "foo": { is recognized, JSONAnnie.SajListener.onStartObject(java.lang.String) is called passing "foo" as the object name.
  • When the sequence "foo": [ is recognized, JSONAnnie.SajListener.onStartArray(java.lang.String) is called passing "foo" as the array name.
  • When a simple object member value is recognized, e.g., "name": "value", the method JSONAnnie.SajListener.onValue(java.lang.String,java.lang.String) is called passing "name" and "value". If the value is the JSON literal null, an empty string is passed as the value. Numbers are also passed as strings, as are the constants true and false.
  • When a simple array value is recognized, e.g., "123" or "xyz", the method JSONAnnie.SajListener.onValue(java.lang.String,java.lang.String) is called passing "value" as the name and string form of value using the same rules as in the case above.
  • When an array value is a "value object" as in the example "foo": [{"bar": "bat"}], the outer curly braces are tossed, after the onArrayStart("foo") call, the next call will be onValue("bar", "bar").
  • Similarly, if an array value is a complex object as in the example "foo": [{"bar": {...stuff...}}], the outer curly braces are stripped. Instead, onObjectStart("bar") is called followed by whatever is appropriate based on the ...stuff... inside the inner curly braces.
  • JSONAnnie.SajListener.onEndObject() and JSONAnnie.SajListener.onEndArray() are called whenever a matching '}' or ']' is found (except for the ignore-curly-braces cases described above).

Why Annie? I have a Jack Russel Terrier named Annie, and she is very fast.

Author(s):
Randy Guck
 
 public class JSONAnnie {
    
 
     public interface SajListener {
        
This method is called when the sequence '"name": {' is recognized.

Parameters:
name Member name that starts an object declaration.
 
         void onStartObject(String name);
        
        
This method is called when the '}' is recognized that matches a onStartObject(java.lang.String) call.
 
         void onEndObject();

        
This method is called when the sequence '"name": [' is recognized.

Parameters:
name Member name that starts a named array declaration.
 
         void onStartArray(String name);
        
        
This method is called when the ']' is recognized that matches a onStartArray(java.lang.String) call.
 
         void onEndArray();
        
        
This method is called when the sequence '"name": "value"' is recognized or when a simple unnamed array value is recognized. In the latter case, the name passed is "value". When the value is JSON literal null, an empty string is passed as the value. Numbers are also passed as strings, and the constants true and false are passed as strings as well.

Parameters:
name Name of named value of "value" for unnamed array values.
value Value parsed, converted to a String.
        void onValue(String nameString value);
    }   // SajListener
    
    // Constants
    private static final int MAX_STACK_DEPTH = 32;
    private static final char EOF = (char)-1;
    // JSON text input source:
    private final JSONInput m_input;
    
    // Stack of nested JSON constructs:
    enum Construct {
        GHOST,      // { } being silently ignore
        OBJECT,     // { } passed to the listener
        ARRAY       // [ ] passed to the listener
    }
    private Construct[] m_stack = new Construct[];
    private int         m_stackPos = 0;
    
    // Current parsing state:
    enum State {
        MEMBER_LIST,
        MEMBER,
        VALUE,
        NEXT,
    }
    private State state;
    ///// JSONInput classes
    
    // Abstract class that encapsulates a JSON input source. It provides methods to get the
    // next character, get the next non-whitespace character, and push a character back for
    // subsequent re-reading. It also provides methods to extract a complete JSON token:
    // string, number, or literal constant.
    private abstract static class JSONInput {
        // Short-term (single method) temporary buffers
        final StringBuilder buffer = new StringBuilder();
        final StringBuilder digitBuffer = new StringBuilder();
        
        // Return next char to parse or EOF
        abstract char nextChar(boolean isEOFAllowed);
        
        // Push back the last character. Only a 1-char pushback is needed.
        abstract void pushBack(char ch);
        
        // Return the next non-whitespace character or EOF. Throw is EOF is reached unexpectedly.
        char nextNonWSChar(boolean isEOFAllowed) {
            char ch = nextChar(isEOFAllowed);
            while (ch !=  && Character.isWhitespace(ch)) {
                ch = nextChar(isEOFAllowed);
            }
            return ch;
        }   // nextNonWSChar
        // Return the quoted string beginning at the given char; error if we don't find one.
        String nextString(char ch) {
            // Decode escape sequences and return unquoted string value
            .setLength(0);
            check(ch == '"'"'\"' expected: " + ch);
            while (true) {
                ch = nextChar(false);
                if (ch == '"') {
                    // Outer closing double quote.
                    break;
                } else if (ch == '\\') {
                    // Escape sequence.
                    ch = nextChar(false);
                    switch (ch) {
                    case 'u':
                        // \uDDDD sequence: Expect four hex digits.
                        .setLength(0);
                        for (int digits = 0; digits < 4; digits++) {
                            ch = nextChar(false);
                            check(Utils.isHexDigit(ch), "Hex digit expected: " + ch);
                            .append(ch);
                        }
                        .append((char)Integer.parseInt(.toString(), 16));
                        break;
                    case '\"'.append('\"'); break;
                    case '\\'.append('\\'); break;
                    case '/':  .append('/');  break;
                    case 'b':  .append('\b'); break;
                    case 'f':  .append('\f'); break;
                    case 'n':  .append('\n'); break;
                    case 'r':  .append('\r'); break;
                    case 't':  .append('\t'); break;
                    default:
                        check(false"Invalid escape sequence: \\" + ch);
                    }
                } else {
                    // All other chars are allowed as-is, despite JSON spec.
                    .append(ch);
                }
            }
            return .toString();
        }   // nextString
        
        // Parse a literal value beginning at the given char; error if we don't find one.
        String nextValue(char ch) {
            if (ch == '"') {
                return nextString(ch);
            }
            if (ch == '-' || (ch >= '0' && ch <= '9')) {
                return nextNumber(ch);
            }
            if (Character.isLetter(ch)) {
                return nextLiteral(ch);
            }
            check(false"Unrecognized start of value: " + ch);
            return null;    // unreachable
        }   // nextValue
        
        // Parse a number literal. Currently we only recognize integers; not exponents.
        String nextNumber(char ch) {
            .setLength(0);
            // First char only can be a "dash".
            if (ch == '-') {
                .append(ch);
                ch = nextChar(false);
            }
            // Accumulate digits
            while (ch >= '0' && ch <= '9') {
                .append(ch);
                ch = nextChar(false);
            }
            // Push back the last non-digit.
            pushBack(ch);
            // Cannot be "-" only
            String value = .toString();
            check(!value.equals("-"), "At least one digit must follow '-' in numeric value");
            return value;
        }   // nextNumber
        
        // Parse a keyword constant: false, true, or null. Case-insensitive.
        String nextLiteral(char ch) {
            .setLength(0);
            while (Character.isLetter(ch)) {
                .append(ch);
                ch = nextChar(false);
            }
            // Push back the last letter.
            pushBack(ch);
            String value = .toString();
            if (value.equalsIgnoreCase("false")) {
                return "false";
            }
            if (value.equalsIgnoreCase("true")) {
                return "true";
            }
            if (value.equalsIgnoreCase("null")) {
                return "";
            }
            check(false"Unrecognized literal: " + value);
            return null;    // not reachable
        }
    }   // class JSONInput
    
    // JSONInput specialization that reads chars from a String.
    private static class JSONInputString extends JSONInput {
        private int index;
        private final String jsonText;
        // Wrap the given string.
        JSONInputString(String text) {
             = text;
        }   // constructor
        
        // Return next char to parse or EOF
        @Override
        char nextChar(boolean isEOFAllowed) {
            if ( >= .length()) {
                check(isEOFAllowed"Unexpected EOF");
                return ;
            }
            return .charAt(++);
        }   // nextChar
        // Push back 1 char.
        @Override
        void pushBack(char ch) {
            assert  > 0;
            --;
            assert .charAt() == ch;
        }   // pushBack
        
    }   // class JSONInputString
    
    // JSONInput specialization that reads chars from a reader.
    private static class JSONInputReader extends JSONInput {
        final Reader reader;
        char pushBack = ;
        // Wrap the given Reader. Note that we don't close it when we're done.
        JSONInputReader(Reader reader) {
            this. = reader;
        }   // constructor
        
        // Use the push back char if present, otherwise use the Reader.
        @Override
        char nextChar(boolean isEOFAllowed) {
            char ch;
            if ( != ) {
                ch = ;
                 = ;
            } else {
                try {
                    ch = (char.read();
                } catch (IOException e) {
                    ch = ;
                }
            }
            check(ch !=  || isEOFAllowed"Unexpected EOF");
            return ch;
        }   // nextChar
        // Push back 1 char, which must be read next.
        @Override
        void pushBack(char ch) {
            assert  ==  : "Only 1 char can be pushed back";
             = ch;
        }   // pushBack
    }   // class JSONInputReader
    
    ///// Public methods
    
    
Create an object that uses the given text as input when parse(com.dell.doradus.common.JSONAnnie.SajListener) is called.

Parameters:
jsonText JSON text string.
    public JSONAnnie(String jsonText) {
        Utils.require(jsonText != null && jsonText.length() > 0, "JSON text cannot be empty");
         = new JSONInputString(jsonText);
    }   // constructor
    
    
Create an object that uses the given reader as input when parse(com.dell.doradus.common.JSONAnnie.SajListener) is called. Note that we don't close the Reader when we're done with it.

Parameters:
reader Open character Reader.
    public JSONAnnie(Reader reader) {
         = new JSONInputReader(reader);
    }   // constructor

    
Parse the JSON given to the constructor and call the given listener as each construct is found. An IllegalArgumentException is thrown if a syntax error or unsupported JSON construct is found.

Parameters:
listener Callback for SAJ eve.ts
Throws:
java.lang.IllegalArgumentException If there's a syntax error.
    public void parse(SajListener listenerthrows IllegalArgumentException {
        assert listener != null;
        
        // We require the first construct to be an object with no leading whitespace.
         = 0;
        char ch = .nextChar(false);
        check(ch == '{'"First character must be '{': " + ch);
        
        // Mark outer '[' with a "ghost" object.
        push(.);
        
        // Enter the state machine parsing the first member name.
         = .;
        boolean bFinished = false;
        String memberName = null;
        String value = null;
        while (!bFinished) {
            switch () {
            case :
                // Expect a quote to start a member or a '}' to denote an empty list.
                ch = .nextNonWSChar(false);
                if (ch != '}') {
                    // Should a quote to start a member name.
                     = .;
                } else {
                    // Empty object list
                    if (pop() == .) {
                        listener.onEndObject();
                    }
                    if (emptyStack()) {
                        bFinished = true;
                    }
                     = .;
                }
                break;
                
            case :
                // Expect an object member: "name": <value>.
                memberName = .nextString(ch);
                
                // Colon should follow
                ch = .nextNonWSChar(false);
                check(ch == ':'"Colon expected: " + ch);
                
                // Member value shoud be next
                ch = .nextNonWSChar(false);
                if (ch == '{') {
                    listener.onStartObject(memberName);
                    push(.);
                     = .;
                } else if (ch == '[') {
                    listener.onStartArray(memberName);
                    push(.);
                     = .;
                } else {
                    // Value must be a literal.
                    value = .nextValue(ch);
                    listener.onValue(memberNamevalue);
                    
                    // Must be followed by next member or object/array closure.
                     = .;
                }
                break;
                
            case :
                // Here, we expect an array value: object or literal.
                ch = .nextNonWSChar(false);
                if (ch == '{') {
                    // Push a GHOST so we eat the outer { } pair.
                    push(.);
                     = .;
                } else if (ch == ']') {
                    // Empty array.
                    listener.onEndArray();
                    pop();
                    if (emptyStack()) {
                        bFinished = true;
                    } else {
                         = .;
                    }
                } else if (ch == '[') {
                    // Value is a nested array, which we don't currently support.
                    check(false"Nested JSON arrays are not supported");
                } else {
                    // Value must be a literal value. It is implicitly named "value"
                    value = .nextValue(ch);
                    listener.onValue("value"value);
                    
                    // Must be followed by next member or object/array terminator
                     = .;
                }
                break;
                
            case :
                // Expect a comma or object/array closure.
                ch = .nextNonWSChar(false);
                Construct tos = tos();
                if (ch == ',') {
                    if (tos == . || tos == .) {
                        ch = .nextNonWSChar(false);
                         = .;
                    } else {
                         = .;
                    }
                } else {
                    switch (tos) {
                    case :
                        check(ch == ']'"']' or ',' expected: " + ch);
                        listener.onEndArray();
                        break;
                    case :
                        check(ch == '}'"'}' or ',' expected: " + ch);
                        break;
                    case :
                        check(ch == '}'"'}' or ',' expected: " + ch);
                        listener.onEndObject();
                        break;
                    }
                    pop();
                    if (emptyStack()) {
                        bFinished = true;
                    }
                    // If not finished, stay in NEXT state.
                }
                break;
                
            default:
                assert false"Missing case for state: " + ;
            }
        }
        
        // Here, we should be at EOF
        ch = .nextNonWSChar(true);
        check(ch == "End of input expected: " + ch);
    }   // parse
    
    ///// Private methods
    // Push the given node onto the node stack.
    private void push(Construct construct) {
        // Make sure we don't blow the stack.
        check( < "Too many JSON nested levels (maximum=" +  + ")");
        [++] = construct;
    }   // push
    // Return the node currently at top of stack or null if there is none.
    private Construct tos() {
        assert  > 0;
        return [ - 1];
    }   // tos
    
    // Pop the stack by one and return the node removed.
    private Construct pop() {
        assert  > 0;
        return [--];
    }   // pop
    
    // Return true if the stack is empty.
    private boolean emptyStack() {
        return  == 0;
    }   // emptyStack
    
    private static void check(boolean conditionString errMsg) {
        Utils.require(conditionerrMsg);
    }   // check
    
}   // class JSONAnnie
New to GrepCode? Check out our FAQ X