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

com.cedarsoftware.util.io.JsonParser Maven / Gradle / Ivy

package com.cedarsoftware.util.io;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
 * Parse the JSON input stream supplied by the FastPushbackReader to the constructor.
 * The entire JSON input stream will be read until it is emptied: an EOF (-1) is read.
 *
 * While reading the content, Java Maps (JsonObjects) are used to hold the contents of
 * JSON objects { }.  Lists are used to hold the contents of JSON arrays.  Each object
 * that has an @id field will be copied into the supplied 'objectsMap' constructor
 * argument.  This allows the user of this class to locate any referenced object
 * directly.
 *
 * When this parser completes, the @ref (references to objects identified with @id)
 * are stored as a JsonObject with an @ref as the key and the ID value of the object.
 * No substitution has yet occurred (substituting the @ref pointers with a Java
 * reference to the actual Map (Map containing the @id)).
 *
 * @author John DeRegnaucourt ([email protected])
 *         
* Copyright (c) Cedar Software LLC *

* 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. */ class JsonParser { public static final String EMPTY_OBJECT = "~!o~"; // compared with == private static final String EMPTY_ARRAY = "~!a~"; // compared with == private static final int STATE_READ_START_OBJECT = 0; private static final int STATE_READ_FIELD = 1; private static final int STATE_READ_VALUE = 2; private static final int STATE_READ_POST_VALUE = 3; private static final Map stringCache = new HashMap(); private final FastPushbackReader input; private final Map objsRead; private final StringBuilder strBuf = new StringBuilder(); private final StringBuilder hexBuf = new StringBuilder(); private final char[] numBuf = new char[256]; private final boolean useMaps; private final Map typeNameMap; static { // Save heap memory by re-using common strings (String's immutable) stringCache.put("", ""); stringCache.put("true", "true"); stringCache.put("True", "True"); stringCache.put("TRUE", "TRUE"); stringCache.put("false", "false"); stringCache.put("False", "False"); stringCache.put("FALSE", "FALSE"); stringCache.put("null", "null"); stringCache.put("yes", "yes"); stringCache.put("Yes", "Yes"); stringCache.put("YES", "YES"); stringCache.put("no", "no"); stringCache.put("No", "No"); stringCache.put("NO", "NO"); stringCache.put("on", "on"); stringCache.put("On", "On"); stringCache.put("ON", "ON"); stringCache.put("off", "off"); stringCache.put("Off", "Off"); stringCache.put("OFF", "OFF"); stringCache.put("@id", "@id"); stringCache.put("@ref", "@ref"); stringCache.put("@items", "@items"); stringCache.put("@type", "@type"); stringCache.put("@keys", "@keys"); stringCache.put("0", "0"); stringCache.put("1", "1"); stringCache.put("2", "2"); stringCache.put("3", "3"); stringCache.put("4", "4"); stringCache.put("5", "5"); stringCache.put("6", "6"); stringCache.put("7", "7"); stringCache.put("8", "8"); stringCache.put("9", "9"); } JsonParser(FastPushbackReader reader, Map objectsMap, Map args) { input = reader; useMaps = Boolean.TRUE.equals(args.get(JsonReader.USE_MAPS)); objsRead = objectsMap; typeNameMap = (Map) args.get(JsonReader.TYPE_NAME_MAP_REVERSE); } private Object readJsonObject() throws IOException { boolean done = false; String field = null; JsonObject object = new JsonObject(); int state = STATE_READ_START_OBJECT; final FastPushbackReader in = input; while (!done) { int c; switch (state) { case STATE_READ_START_OBJECT: c = skipWhitespaceRead(); if (c == '{') { object.line = in.line; object.col = in.col; c = skipWhitespaceRead(); if (c == '}') { // empty object return EMPTY_OBJECT; } in.unread(c); state = STATE_READ_FIELD; } else { // The line below is not technically required, however, without it, the tests run // twice as slow. It is apparently affecting a word, or paragraph boundary where // the generated code sits, making it much faster. objsRead.size(); error("Input is invalid JSON; object does not start with '{', c=" + c); } break; case STATE_READ_FIELD: c = skipWhitespaceRead(); if (c == '"') { field = readString(); c = skipWhitespaceRead(); if (c != ':') { error("Expected ':' between string field and value"); } skipWhitespace(); if (field.startsWith("@")) { // Expand short-hand meta keys if (field.equals("@t")) { field = stringCache.get("@type"); } else if (field.equals("@i")) { field = stringCache.get("@id"); } else if (field.equals("@r")) { field = stringCache.get("@ref"); } else if (field.equals("@k")) { field = stringCache.get("@keys"); } else if (field.equals("@e")) { field = stringCache.get("@items"); } } state = STATE_READ_VALUE; } else { error("Expected quote"); } break; case STATE_READ_VALUE: if (field == null) { // field is null when you have an untyped Object[], so we place // the JsonArray on the @items field. field = "@items"; } Object value = readValue(object); if ("@type".equals(field) && typeNameMap != null) { final String substitute = typeNameMap.get(value); if (substitute != null) { value = substitute; } } object.put(field, value); // If object is referenced (has @id), then put it in the _objsRead table. if ("@id".equals(field)) { objsRead.put((Long) value, object); } state = STATE_READ_POST_VALUE; break; case STATE_READ_POST_VALUE: c = skipWhitespaceRead(); if (c == -1) { error("EOF reached before closing '}'"); } if (c == '}') { done = true; } else if (c == ',') { state = STATE_READ_FIELD; } else { error("Object not ended with '}'"); } break; } } if (useMaps && object.isPrimitive()) { return object.getPrimitiveValue(); } return object; } Object readValue(JsonObject object) throws IOException { final int c = input.read(); switch(c) { case '"': return readString(); case '{': input.unread('{'); return readJsonObject(); case '[': return readArray(object); case ']': // empty array input.unread(']'); return EMPTY_ARRAY; case 'f': case 'F': readToken("false"); return Boolean.FALSE; case 'n': case 'N': readToken("null"); return null; case 't': case 'T': readToken("true"); return Boolean.TRUE; case -1: error("EOF reached prematurely"); } if (c >= '0' && c <= '9' || c == '-') { return readNumber(c); } return error("Unknown JSON value type"); } /** * Read a JSON array */ private Object readArray(JsonObject object) throws IOException { final Collection array = new ArrayList(); while (true) { skipWhitespace(); final Object o = readValue(object); if (o != EMPTY_ARRAY) { array.add(o); } final int c = skipWhitespaceRead(); if (c == ']') { break; } else if (c != ',') { error("Expected ',' or ']' inside array"); } } return array.toArray(); } /** * Return the specified token from the reader. If it is not found, * throw an IOException indicating that. Converting to c to * (char) c is acceptable because the 'tokens' allowed in a * JSON input stream (true, false, null) are all ASCII. */ private void readToken(String token) throws IOException { final int len = token.length(); for (int i = 1; i < len; i++) { int c = input.read(); if (c == -1) { error("EOF reached while reading token: " + token); } c = Character.toLowerCase((char) c); int loTokenChar = token.charAt(i); if (loTokenChar != c) { error("Expected token: " + token); } } } /** * Read a JSON number * * @param c int a character representing the first digit of the number that * was already read. * @return a Number (a Long or a Double) depending on whether the number is * a decimal number or integer. This choice allows all smaller types (Float, int, short, byte) * to be represented as well. * @throws IOException for stream errors or parsing errors. */ private Number readNumber(int c) throws IOException { final FastPushbackReader in = input; final char[] buffer = this.numBuf; buffer[0] = (char) c; int len = 1; boolean isFloat = false; try { while (true) { c = in.read(); if ((c >= '0' && c <= '9') || c == '-' || c == '+') // isDigit() inlined for speed here { buffer[len++] = (char) c; } else if (c == '.' || c == 'e' || c == 'E') { buffer[len++] = (char) c; isFloat = true; } else if (c == -1) { break; } else { in.unread(c); break; } } } catch (ArrayIndexOutOfBoundsException e) { error("Too many digits in number: " + new String(buffer)); } if (isFloat) { // Floating point number needed String num = new String(buffer, 0, len); try { return Double.parseDouble(num); } catch (NumberFormatException e) { error("Invalid floating point number: " + num, e); } } boolean isNeg = buffer[0] == '-'; long n = 0; for (int i = isNeg ? 1 : 0; i < len; i++) { n = (buffer[i] - '0') + n * 10; } return isNeg ? -n : n; } private static final int STATE_STRING_START = 0; private static final int STATE_STRING_SLASH = 1; private static final int STATE_HEX_DIGITS_START = 2; private static final int STATE_HEX_DIGITS = 3; /** * Read a JSON string * This method assumes the initial quote has already been read. * * @return String read from JSON input stream. * @throws IOException for stream errors or parsing errors. */ private String readString() throws IOException { final StringBuilder str = this.strBuf; str.setLength(0); boolean done = false; int state = STATE_STRING_START; while (!done) { final int c = input.read(); if (c == -1) { error("EOF reached while reading JSON string"); } switch (state) { case STATE_STRING_START: if (c == '"') { done = true; } else if (c == '\\') { state = STATE_STRING_SLASH; } else { str.appendCodePoint(c); } break; case STATE_STRING_SLASH: switch(c) { case '\\': str.append('\\'); break; case '/': str.append('/'); break; case '"': str.append('"'); break; case '\'': str.append('\''); break; case 'b': str.append('\b'); break; case 'f': str.append('\f'); break; case 'n': str.append('\n'); break; case 'r': str.append('\r'); break; case 't': str.append('\t'); break; case 'u': state = STATE_HEX_DIGITS_START; break; default: error("Invalid character escape sequence specified: " + c); } if (c != 'u') { state = STATE_STRING_START; } break; case STATE_HEX_DIGITS_START: hexBuf.setLength(0); state = STATE_HEX_DIGITS; // intentional 'fall-thru' case STATE_HEX_DIGITS: switch(c) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': hexBuf.append((char) c); if (hexBuf.length() == 4) { int value = Integer.parseInt(hexBuf.toString(), 16); str.append(MetaUtils.valueOf((char) value)); state = STATE_STRING_START; } break; default: error("Expected hexadecimal digits"); } break; } } final String s = str.toString(); final String cacheHit = stringCache.get(s); return cacheHit == null ? s : cacheHit; } /** * Read until non-whitespace character and then return it. * This saves extra read/pushback. * * @return int representing the next non-whitespace character in the stream. * @throws IOException for stream errors or parsing errors. */ private int skipWhitespaceRead() throws IOException { final FastPushbackReader in = input; int c = in.read(); while (true) { switch (c) { case '\t': case '\n': case '\r': case ' ': break; default: return c; } c = in.read(); } } private void skipWhitespace() throws IOException { input.unread(skipWhitespaceRead()); } Object error(String msg) { throw new JsonIoException(getMessage(msg)); } Object error(String msg, Exception e) { throw new JsonIoException(getMessage(msg), e); } String getMessage(String msg) { return msg + "\nline: " + input.line + ", col: " + input.col + "\n" + input.getLastSnippet(); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy