com.cedarsoftware.io.JsonParser Maven / Gradle / Ivy
package com.cedarsoftware.io;
import java.io.IOException;
import java.io.Reader;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.cedarsoftware.io.reflect.Injector;
import com.cedarsoftware.util.ClassUtilities;
import com.cedarsoftware.util.FastReader;
import static com.cedarsoftware.io.JsonObject.ID;
import static com.cedarsoftware.io.JsonObject.ITEMS;
import static com.cedarsoftware.io.JsonObject.KEYS;
import static com.cedarsoftware.io.JsonObject.REF;
import static com.cedarsoftware.io.JsonObject.SHORT_ID;
import static com.cedarsoftware.io.JsonObject.SHORT_ITEMS;
import static com.cedarsoftware.io.JsonObject.SHORT_KEYS;
import static com.cedarsoftware.io.JsonObject.SHORT_REF;
import static com.cedarsoftware.io.JsonObject.SHORT_TYPE;
import static com.cedarsoftware.io.JsonObject.TYPE;
import static com.cedarsoftware.util.MathUtilities.parseToMinimalNumericType;
/**
* 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 a @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
*
* License
*
* 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 {
private static final JsonObject EMPTY_ARRAY = new JsonObject(); // compared with ==
private final Map stringCache = new LinkedHashMap() {
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > 2500;
}
};
private final Map numberCache = new LinkedHashMap() {
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > 2500;
}
};
private final Map substitutes = new LinkedHashMap<>();
private final FastReader input;
private final StringBuilder strBuf = new StringBuilder(256);
private final StringBuilder hexBuf = new StringBuilder();
private final StringBuilder numBuf = new StringBuilder();
private int curParseDepth = 0;
private final boolean allowNanAndInfinity;
private final int maxParseDepth;
private final ReadOptions readOptions;
private final ReferenceTracker references;
{
// substitutes
substitutes.put(SHORT_ID, ID);
substitutes.put(SHORT_REF, REF);
substitutes.put(SHORT_ITEMS, ITEMS);
substitutes.put(SHORT_TYPE, TYPE);
substitutes.put(SHORT_KEYS, KEYS);
// 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");
numberCache.put(-1L, -1L);
numberCache.put(0L, 0L);
numberCache.put(1L, 1L);
numberCache.put(-1.0d, -1.0d);
numberCache.put(0.0d, 0.0d);
numberCache.put(1.0d, 1.0d);
numberCache.put(Double.MIN_VALUE, Double.MIN_VALUE);
numberCache.put(Double.MAX_VALUE, Double.MAX_VALUE);
numberCache.put(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY);
numberCache.put(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY);
numberCache.put(Double.NaN, Double.NaN);
}
JsonParser(FastReader reader, Resolver resolver) {
input = reader;
readOptions = resolver.getReadOptions();
references = resolver.getReferences();
maxParseDepth = readOptions.getMaxDepth();
allowNanAndInfinity = readOptions.isAllowNanAndInfinity();
}
/**
* Read a JSON value (see json.org). A value can be a JSON object, array, string, number, ("true", "false"), or "null".
* @param suggestedClass JsonValue Owning entity.
*/
Object readValue(Class> suggestedClass) throws IOException {
if (curParseDepth > maxParseDepth) {
error("Maximum parsing depth exceeded");
}
int c = skipWhitespaceRead(true);
if (c >= '0' && c <= '9' || c == '-' || c == 'N' || c == 'I') {
return readNumber(c);
}
switch (c) {
case '"':
String str = readString();
return str;
case '{':
input.pushback('{');
JsonObject jObj = readJsonObject(suggestedClass);
return jObj;
case '[':
Object[] array = readArray(suggestedClass == null ? null : suggestedClass.getComponentType());
return array;
case ']': // empty array
input.pushback(']');
return EMPTY_ARRAY;
case 'f':
case 'F':
readToken("false");
return false;
case 'n':
case 'N':
readToken("null");
return null;
case 't':
case 'T':
readToken("true");
return true;
}
return error("Unknown JSON value type");
}
/**
* Read a JSON object { ... }
*
* @return JsonObject that represents the { ... } being read in. If the JSON object type can be inferred,
* from an @type field, containing field type, or containing array type, then the javaType will be set on the
* JsonObject.
*/
private JsonObject readJsonObject(Class> suggestedClass) throws IOException {
JsonObject jObj = new JsonObject();
jObj.setHintType(suggestedClass);
final FastReader in = input;
// Start reading the object, skip white space and find {
skipWhitespaceRead(true); // Burn '{'
jObj.line = in.getLine();
jObj.col = in.getCol();
int c = skipWhitespaceRead(true);
if (c == '}') { // empty object
// Using new JsonObject() below will prevent @id/@ref if more than one {} appears in the JSON.
return new JsonObject();
}
in.pushback((char) c);
++curParseDepth;
Map injectors = readOptions.getDeepInjectorMap(suggestedClass);
while (true) {
String field = readFieldName();
if (substitutes.containsKey(field)) {
field = substitutes.get(field);
}
Injector injector = injectors.get(field);
Object value = readValue(injector == null ? null : injector.getType());
// process key-value pairing
switch (field) {
case TYPE:
Class> type = loadType(value); // TODO: Can we remove @enum and fold that into @type?
jObj.setJavaType(type);
break;
case REF:
loadRef(value, jObj);
break;
case ID:
loadId(value, jObj);
break;
default:
jObj.put(field, value); // Load key/value pair
break;
}
c = skipWhitespaceRead(true);
if (c == '}') {
break;
} else if (c != ',') {
error("Object not ended with '}', instead found '" + (char) c + "'");
}
}
--curParseDepth;
return jObj;
}
/**
* Read the field name of a JSON object.
*
* @return String field name.
*/
private String readFieldName() throws IOException {
int c = skipWhitespaceRead(true);
if (c != '"') {
error("Expected quote before field name");
}
String field = readString();
c = skipWhitespaceRead(true);
if (c != ':') {
error("Expected ':' between field and value, instead found '" + (char) c + "'");
}
return field;
}
/**
* Read a JSON array
*/
private Object[] readArray(Class> suggestedClass) throws IOException {
final List