
com.cedarsoftware.io.JsonParser Maven / Gradle / Ivy
package com.cedarsoftware.io;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import com.cedarsoftware.io.reflect.Injector;
import com.cedarsoftware.util.ArrayUtilities;
import com.cedarsoftware.util.ClassUtilities;
import com.cedarsoftware.util.FastReader;
import com.cedarsoftware.util.TypeUtilities;
import static com.cedarsoftware.io.JsonObject.ENUM;
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 FastReader input;
private final StringBuilder strBuf = new StringBuilder(256);
private final StringBuilder numBuf = new StringBuilder();
private int curParseDepth = 0;
private final boolean allowNanAndInfinity;
private final int maxParseDepth;
private final Resolver resolver;
private final ReadOptions readOptions;
private final ReferenceTracker references;
// Instance-level cache for parser-specific strings
private final Map stringCache;
private final Map numberCache;
private final Map substitutes;
private static final ThreadLocal STRING_BUFFER = ThreadLocal.withInitial(() -> new char[4096]);
// Primary static cache that never changes
private static final Map STATIC_STRING_CACHE = new ConcurrentHashMap<>(64);
private static final Map STATIC_NUMBER_CACHE = new ConcurrentHashMap<>(16);
private static final Map SUBSTITUTES = new HashMap<>(5);
// Static lookup tables for performance
private static final char[] ESCAPE_CHAR_MAP = new char[128];
private static final int[] HEX_VALUE_MAP = new int[128];
static {
// Initialize escape character map
ESCAPE_CHAR_MAP['\\'] = '\\';
ESCAPE_CHAR_MAP['/'] = '/';
ESCAPE_CHAR_MAP['"'] = '"';
ESCAPE_CHAR_MAP['\''] = '\'';
ESCAPE_CHAR_MAP['b'] = '\b';
ESCAPE_CHAR_MAP['f'] = '\f';
ESCAPE_CHAR_MAP['n'] = '\n';
ESCAPE_CHAR_MAP['r'] = '\r';
ESCAPE_CHAR_MAP['t'] = '\t';
// Initialize hex value map
Arrays.fill(HEX_VALUE_MAP, -1);
for (int i = '0'; i <= '9'; i++) {
HEX_VALUE_MAP[i] = i - '0';
}
for (int i = 'a'; i <= 'f'; i++) {
HEX_VALUE_MAP[i] = 10 + (i - 'a');
}
for (int i = 'A'; i <= 'F'; i++) {
HEX_VALUE_MAP[i] = 10 + (i - 'A');
}
// Initialize substitutions
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);
// Common strings
String[] commonStrings = {
"", "true", "True", "TRUE", "false", "False", "FALSE",
"null", "yes", "Yes", "YES", "no", "No", "NO",
"on", "On", "ON", "off", "Off", "OFF",
"id", "ID", "type", "value", "name",
ID, REF, ITEMS, TYPE, KEYS,
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"
};
for (String s : commonStrings) {
STATIC_STRING_CACHE.put(s, s);
}
// Common numbers
STATIC_NUMBER_CACHE.put(-1L, -1L);
STATIC_NUMBER_CACHE.put(0L, 0L);
STATIC_NUMBER_CACHE.put(1L, 1L);
STATIC_NUMBER_CACHE.put(-1.0d, -1.0d);
STATIC_NUMBER_CACHE.put(0.0d, 0.0d);
STATIC_NUMBER_CACHE.put(1.0d, 1.0d);
STATIC_NUMBER_CACHE.put(Double.MIN_VALUE, Double.MIN_VALUE);
STATIC_NUMBER_CACHE.put(Double.MAX_VALUE, Double.MAX_VALUE);
STATIC_NUMBER_CACHE.put(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY);
STATIC_NUMBER_CACHE.put(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY);
STATIC_NUMBER_CACHE.put(Double.NaN, Double.NaN);
}
// Wrapper class for efficient two-tier string caching
private static class ParserStringCache extends AbstractMap {
private final Map staticCache;
private final Map instanceCache;
public ParserStringCache(Map staticCache) {
this.staticCache = staticCache;
this.instanceCache = new HashMap<>(64); // Instance-specific cache
}
@Override
public String get(Object key) {
// First check static cache (no synchronization needed)
String result = staticCache.get(key);
if (result != null) {
return result;
}
// Then check instance-specific cache
return instanceCache.get(key);
}
@Override
public String put(String key, String value) {
// Don't modify static cache
return instanceCache.put(key, value);
}
// Implementation of other required methods...
@Override
public Set> entrySet() {
// Merge both caches for entrySet view
Set> entries = new HashSet<>();
entries.addAll(staticCache.entrySet());
entries.addAll(instanceCache.entrySet());
return entries;
}
}
// Wrapper class for efficient two-tier string caching
private static class ParserNumberCache extends AbstractMap {
private final Map staticCache;
private final Map instanceCache;
public ParserNumberCache(Map staticCache) {
this.staticCache = staticCache;
this.instanceCache = new HashMap<>(64); // Instance-specific cache
}
@Override
public Number get(Object key) {
// First check static cache (no synchronization needed)
Number result = staticCache.get(key);
if (result != null) {
return result;
}
// Then check instance-specific cache
return instanceCache.get(key);
}
@Override
public Number put(Number key, Number value) {
// Don't modify static cache
return instanceCache.put(key, value);
}
// Implementation of other required methods...
@Override
public Set> entrySet() {
// Merge both caches for entrySet view
Set> entries = new HashSet<>();
entries.addAll(staticCache.entrySet());
entries.addAll(instanceCache.entrySet());
return entries;
}
}
JsonParser(FastReader reader, Resolver resolver) {
// Reference the static caches
// For substitutes, use the static map directly (read-only)
this.substitutes = SUBSTITUTES;
// For caches that may grow during parsing, create a wrapper
this.stringCache = new ParserStringCache(STATIC_STRING_CACHE);
this.numberCache = new ParserNumberCache(STATIC_NUMBER_CACHE);
input = reader;
this.resolver = resolver;
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 suggestedType JsonValue Owning entity.
*/
Object readValue(Type suggestedType) 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(suggestedType);
return jObj;
case '[':
Type elementType = TypeUtilities.extractArrayComponentType(suggestedType);
return readArray(elementType);
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(Type suggestedType) throws IOException {
JsonObject jObj = new JsonObject();
// Set the refined type on the JsonObject.
Type resolvedSuggestedType = TypeUtilities.resolveType(suggestedType, suggestedType);
jObj.setType(resolvedSuggestedType);
final FastReader in = input;
// Start reading the object: skip whitespace and consume '{'
skipWhitespaceRead(true); // Consume the '{'
jObj.line = in.getLine();
jObj.col = in.getCol();
int c = skipWhitespaceRead(true);
if (c == '}') { // empty object
// Return a new, empty JsonObject (prevents @id/@ref from interfering)
return new JsonObject();
}
in.pushback((char) c);
++curParseDepth;
// Obtain the injector map.
Map injectors = readOptions.getDeepInjectorMap(TypeUtilities.getRawClass(suggestedType));
while (true) {
String field = readFieldName();
if (substitutes.containsKey(field)) {
field = substitutes.get(field);
}
// For each field, look up the injector.
Injector injector = injectors.get(field);
Type fieldGenericType = injector == null ? null : injector.getGenericType();
// If a field generic type is provided, resolve it using the parent's (i.e. jObj's) resolved type.
if (fieldGenericType != null) {
// Use the parent's type (which has been resolved) as context to resolve the field type.
fieldGenericType = TypeUtilities.resolveType(suggestedType, fieldGenericType);
}
Object value = readValue(fieldGenericType);
// Process key-value pairing.
switch (field) {
case TYPE:
Class> type = loadType(value);
jObj.setTypeString((String) value);
jObj.setType(type);
break;
case ENUM: // Legacy support (@enum was used to indicate EnumSet in prior versions)
loadEnum(value, jObj);
break;
case REF:
loadRef(value, jObj);
break;
case ID:
loadId(value, jObj);
break;
case ITEMS:
loadItems((Object[])value, jObj);
break;
case KEYS:
loadKeys(value, jObj);
break;
default:
jObj.put(field, value); // Store the 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 a JSON array
*/
private Object readArray(Type suggestedType) throws IOException {
final List