// *************************************************************************************************************************** | |
// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * | |
// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * | |
// * to you 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 org.apache.juneau.json; | |
import static org.apache.juneau.internal.StringUtils.*; | |
import java.io.*; | |
import java.lang.reflect.*; | |
import java.util.*; | |
import org.apache.juneau.*; | |
import org.apache.juneau.internal.*; | |
import org.apache.juneau.parser.*; | |
import org.apache.juneau.transform.*; | |
/** | |
* Session object that lives for the duration of a single use of {@link JsonParser}. | |
* | |
* <p> | |
* This class is NOT thread safe. | |
* It is typically discarded after one-time use although it can be reused against multiple inputs. | |
*/ | |
@SuppressWarnings({ "unchecked", "rawtypes" }) | |
public final class JsonParserSession extends ReaderParserSession { | |
private static final AsciiSet decChars = AsciiSet.create().ranges("0-9").build(); | |
private final JsonParser ctx; | |
/** | |
* Create a new session using properties specified in the context. | |
* | |
* @param ctx | |
* The context creating this session object. | |
* The context contains all the configuration settings for this object. | |
* @param args | |
* Runtime session arguments. | |
*/ | |
protected JsonParserSession(JsonParser ctx, ParserSessionArgs args) { | |
super(ctx, args); | |
this.ctx = ctx; | |
} | |
/** | |
* Returns <jk>true</jk> if the specified character is whitespace. | |
* | |
* <p> | |
* The definition of whitespace is different for strict vs lax mode. | |
* Strict mode only interprets 0x20 (space), 0x09 (tab), 0x0A (line feed) and 0x0D (carriage return) as whitespace. | |
* Lax mode uses {@link Character#isWhitespace(int)} to make the determination. | |
* | |
* @param cp The codepoint. | |
* @return <jk>true</jk> if the specified character is whitespace. | |
*/ | |
protected final boolean isWhitespace(int cp) { | |
if (isStrict()) | |
return cp <= 0x20 && (cp == 0x09 || cp == 0x0A || cp == 0x0D || cp == 0x20); | |
return Character.isWhitespace(cp); | |
} | |
/** | |
* Returns <jk>true</jk> if the specified character is whitespace or '/'. | |
* | |
* @param cp The codepoint. | |
* @return <jk>true</jk> if the specified character is whitespace or '/'. | |
*/ | |
protected final boolean isCommentOrWhitespace(int cp) { | |
if (cp == '/') | |
return true; | |
if (isStrict()) | |
return cp <= 0x20 && (cp == 0x09 || cp == 0x0A || cp == 0x0D || cp == 0x20); | |
return Character.isWhitespace(cp); | |
} | |
@Override /* ParserSession */ | |
protected <T> T doParse(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException { | |
try (ParserReader r = pipe.getParserReader()) { | |
if (r == null) | |
return null; | |
T o = parseAnything(type, r, getOuter(), null); | |
validateEnd(r); | |
return o; | |
} | |
} | |
@Override /* ReaderParserSession */ | |
protected <K,V> Map<K,V> doParseIntoMap(ParserPipe pipe, Map<K,V> m, Type keyType, Type valueType) throws IOException, ParseException, ExecutableException { | |
try (ParserReader r = pipe.getParserReader()) { | |
m = parseIntoMap2(r, m, (ClassMeta<K>)getClassMeta(keyType), (ClassMeta<V>)getClassMeta(valueType), null); | |
validateEnd(r); | |
return m; | |
} | |
} | |
@Override /* ReaderParserSession */ | |
protected <E> Collection<E> doParseIntoCollection(ParserPipe pipe, Collection<E> c, Type elementType) throws IOException, ParseException, ExecutableException { | |
try (ParserReader r = pipe.getParserReader()) { | |
c = parseIntoCollection2(r, c, getClassMeta(elementType), null); | |
validateEnd(r); | |
return c; | |
} | |
} | |
private <T> T parseAnything(ClassMeta<?> eType, ParserReader r, Object outer, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException { | |
if (eType == null) | |
eType = object(); | |
PojoSwap<T,Object> swap = (PojoSwap<T,Object>)eType.getPojoSwap(this); | |
BuilderSwap<T,Object> builder = (BuilderSwap<T,Object>)eType.getBuilderSwap(this); | |
ClassMeta<?> sType = null; | |
if (builder != null) | |
sType = builder.getBuilderClassMeta(this); | |
else if (swap != null) | |
sType = swap.getSwapClassMeta(this); | |
else | |
sType = eType; | |
if (sType.isOptional()) | |
return (T)Optional.ofNullable(parseAnything(eType.getElementType(), r, outer, pMeta)); | |
setCurrentClass(sType); | |
String wrapperAttr = sType.getExtendedMeta(JsonClassMeta.class).getWrapperAttr(); | |
Object o = null; | |
skipCommentsAndSpace(r); | |
if (wrapperAttr != null) | |
skipWrapperAttrStart(r, wrapperAttr); | |
int c = r.peek(); | |
if (c == -1) { | |
if (isStrict()) | |
throw new ParseException(this, "Empty input."); | |
// Let o be null. | |
} else if ((c == ',' || c == '}' || c == ']')) { | |
if (isStrict()) | |
throw new ParseException(this, "Missing value detected."); | |
// Handle bug in Cognos 10.2.1 that can product non-existent values. | |
// Let o be null; | |
} else if (c == 'n') { | |
parseKeyword("null", r); | |
} else if (sType.isObject()) { | |
if (c == '{') { | |
ObjectMap m2 = new ObjectMap(this); | |
parseIntoMap2(r, m2, string(), object(), pMeta); | |
o = cast(m2, pMeta, eType); | |
} else if (c == '[') { | |
o = parseIntoCollection2(r, new ObjectList(this), object(), pMeta); | |
} else if (c == '\'' || c == '"') { | |
o = parseString(r); | |
if (sType.isChar()) | |
o = parseCharacter(o); | |
} else if (c >= '0' && c <= '9' || c == '-' || c == '.') { | |
o = parseNumber(r, null); | |
} else if (c == 't') { | |
parseKeyword("true", r); | |
o = Boolean.TRUE; | |
} else { | |
parseKeyword("false", r); | |
o = Boolean.FALSE; | |
} | |
} else if (sType.isBoolean()) { | |
o = parseBoolean(r); | |
} else if (sType.isCharSequence()) { | |
o = parseString(r); | |
} else if (sType.isChar()) { | |
o = parseCharacter(parseString(r)); | |
} else if (sType.isNumber()) { | |
o = parseNumber(r, (Class<? extends Number>)sType.getInnerClass()); | |
} else if (sType.isMap()) { | |
Map m = (sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : new ObjectMap(this)); | |
o = parseIntoMap2(r, m, sType.getKeyType(), sType.getValueType(), pMeta); | |
} else if (sType.isCollection()) { | |
if (c == '{') { | |
ObjectMap m = new ObjectMap(this); | |
parseIntoMap2(r, m, string(), object(), pMeta); | |
o = cast(m, pMeta, eType); | |
} else { | |
Collection l = (sType.canCreateNewInstance(outer) ? (Collection)sType.newInstance() : new ObjectList(this)); | |
o = parseIntoCollection2(r, l, sType, pMeta); | |
} | |
} else if (builder != null) { | |
BeanMap m = toBeanMap(builder.create(this, eType)); | |
o = builder.build(this, parseIntoBeanMap2(r, m).getBean(), eType); | |
} else if (sType.canCreateNewBean(outer)) { | |
BeanMap m = newBeanMap(outer, sType.getInnerClass()); | |
o = parseIntoBeanMap2(r, m).getBean(); | |
} else if (sType.canCreateNewInstanceFromString(outer) && (c == '\'' || c == '"')) { | |
o = sType.newInstanceFromString(outer, parseString(r)); | |
} else if (sType.isArray() || sType.isArgs()) { | |
if (c == '{') { | |
ObjectMap m = new ObjectMap(this); | |
parseIntoMap2(r, m, string(), object(), pMeta); | |
o = cast(m, pMeta, eType); | |
} else { | |
ArrayList l = (ArrayList)parseIntoCollection2(r, new ArrayList(), sType, pMeta); | |
o = toArray(sType, l); | |
} | |
} else if (c == '{') { | |
Map m = new ObjectMap(this); | |
parseIntoMap2(r, m, sType.getKeyType(), sType.getValueType(), pMeta); | |
if (m.containsKey(getBeanTypePropertyName(eType))) | |
o = cast((ObjectMap)m, pMeta, eType); | |
else | |
throw new ParseException(this, "Class ''{0}'' could not be instantiated. Reason: ''{1}''", | |
sType.getInnerClass().getName(), sType.getNotABeanReason()); | |
} else if (sType.canCreateNewInstanceFromString(outer) && ! isStrict()) { | |
o = sType.newInstanceFromString(outer, parseString(r)); | |
} else { | |
throw new ParseException(this, "Unrecognized syntax for class type ''{0}'', starting character ''{1}''", | |
sType, (char)c); | |
} | |
if (wrapperAttr != null) | |
skipWrapperAttrEnd(r); | |
if (swap != null && o != null) | |
o = unswap(swap, o, eType); | |
if (outer != null) | |
setParent(eType, o, outer); | |
return (T)o; | |
} | |
private Number parseNumber(ParserReader r, Class<? extends Number> type) throws IOException, ParseException { | |
int c = r.peek(); | |
if (c == '\'' || c == '"') | |
return parseNumber(r, parseString(r), type); | |
return parseNumber(r, parseNumberString(r), type); | |
} | |
private Number parseNumber(ParserReader r, String s, Class<? extends Number> type) throws ParseException { | |
// JSON has slightly different number rules from Java. | |
// Strict mode enforces these different rules, lax does not. | |
if (isStrict()) { | |
// Lax allows blank strings to represent 0. | |
// Strict does not allow blank strings. | |
if (s.length() == 0) | |
throw new ParseException(this, "Invalid JSON number: ''{0}''", s); | |
// Need to weed out octal and hexadecimal formats: 0123,-0123,0x123,-0x123. | |
// Don't weed out 0 or -0. | |
boolean isNegative = false; | |
char c = s.charAt(0); | |
if (c == '-') { | |
isNegative = true; | |
c = (s.length() == 1 ? 'x' : s.charAt(1)); | |
} | |
// JSON doesn't allow '.123' and '-.123'. | |
if (c == '.') | |
throw new ParseException(this, "Invalid JSON number: ''{0}''", s); | |
// '01' is not a valid number, but '0.1', '0e1', '0e+1' are valid. | |
if (c == '0' && s.length() > (isNegative ? 2 : 1)) { | |
char c2 = s.charAt((isNegative ? 2 : 1)); | |
if (c2 != '.' && c2 != 'e' && c2 != 'E') | |
throw new ParseException(this, "Invalid JSON number: ''{0}''", s); | |
} | |
// JSON doesn't allow '1.' or '0.e1'. | |
int i = s.indexOf('.'); | |
if (i != -1 && (s.length() == (i+1) || ! decChars.contains(s.charAt(i+1)))) | |
throw new ParseException(this, "Invalid JSON number: ''{0}''", s); | |
} | |
return StringUtils.parseNumber(s, type); | |
} | |
private Boolean parseBoolean(ParserReader r) throws IOException, ParseException { | |
int c = r.peek(); | |
if (c == '\'' || c == '"') | |
return Boolean.valueOf(parseString(r)); | |
if (c == 't') { | |
parseKeyword("true", r); | |
return Boolean.TRUE; | |
} else if (c == 'f') { | |
parseKeyword("false", r); | |
return Boolean.FALSE; | |
} else { | |
throw new ParseException(this, "Unrecognized syntax. Expected boolean value, actual=''{0}''", r.read(100)); | |
} | |
} | |
private <K,V> Map<K,V> parseIntoMap2(ParserReader r, Map<K,V> m, ClassMeta<K> keyType, | |
ClassMeta<V> valueType, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException { | |
if (keyType == null) | |
keyType = (ClassMeta<K>)string(); | |
int S0=0; // Looking for outer { | |
int S1=1; // Looking for attrName start. | |
int S3=3; // Found attrName end, looking for :. | |
int S4=4; // Found :, looking for valStart: { [ " ' LITERAL. | |
int S5=5; // Looking for , or } | |
int S6=6; // Found , looking for attr start. | |
skipCommentsAndSpace(r); | |
int state = S0; | |
String currAttr = null; | |
int c = 0; | |
while (c != -1) { | |
c = r.read(); | |
if (state == S0) { | |
if (c == '{') | |
state = S1; | |
else | |
break; | |
} else if (state == S1) { | |
if (c == '}') { | |
return m; | |
} else if (isCommentOrWhitespace(c)) { | |
skipCommentsAndSpace(r.unread()); | |
} else { | |
currAttr = parseFieldName(r.unread()); | |
state = S3; | |
} | |
} else if (state == S3) { | |
if (c == ':') | |
state = S4; | |
} else if (state == S4) { | |
if (isCommentOrWhitespace(c)) { | |
skipCommentsAndSpace(r.unread()); | |
} else { | |
K key = convertAttrToType(m, currAttr, keyType); | |
V value = parseAnything(valueType, r.unread(), m, pMeta); | |
setName(valueType, value, key); | |
m.put(key, value); | |
state = S5; | |
} | |
} else if (state == S5) { | |
if (c == ',') { | |
state = S6; | |
} else if (isCommentOrWhitespace(c)) { | |
skipCommentsAndSpace(r.unread()); | |
} else if (c == '}') { | |
return m; | |
} else { | |
break; | |
} | |
} else if (state == S6) { | |
if (c == '}') { | |
break; | |
} else if (isCommentOrWhitespace(c)) { | |
skipCommentsAndSpace(r.unread()); | |
} else { | |
currAttr = parseFieldName(r.unread()); | |
state = S3; | |
} | |
} | |
} | |
if (state == S0) | |
throw new ParseException(this, "Expected '{' at beginning of JSON object."); | |
if (state == S1) | |
throw new ParseException(this, "Could not find attribute name on JSON object."); | |
if (state == S3) | |
throw new ParseException(this, "Could not find ':' following attribute name on JSON object."); | |
if (state == S4) | |
throw new ParseException(this, "Expected one of the following characters: {,[,',\",LITERAL."); | |
if (state == S5) | |
throw new ParseException(this, "Could not find '}' marking end of JSON object."); | |
if (state == S6) | |
throw new ParseException(this, "Unexpected '}' found in JSON object."); | |
return null; // Unreachable. | |
} | |
/* | |
* Parse a JSON attribute from the character array at the specified position, then | |
* set the position marker to the last character in the field name. | |
*/ | |
private String parseFieldName(ParserReader r) throws IOException, ParseException { | |
int c = r.peek(); | |
if (c == '\'' || c == '"') | |
return parseString(r); | |
if (isStrict()) | |
throw new ParseException(this, "Unquoted attribute detected."); | |
if (! VALID_BARE_CHARS.contains(c)) | |
throw new ParseException(this, "Could not find the start of the field name."); | |
r.mark(); | |
// Look for whitespace. | |
while (c != -1) { | |
c = r.read(); | |
if (! VALID_BARE_CHARS.contains(c)) { | |
r.unread(); | |
String s = r.getMarked().intern(); | |
return s.equals("null") ? null : s; | |
} | |
} | |
throw new ParseException(this, "Could not find the end of the field name."); | |
} | |
private static final AsciiSet VALID_BARE_CHARS = AsciiSet.create().range('A','Z').range('a','z').range('0','9').chars("$_-.").build(); | |
private <E> Collection<E> parseIntoCollection2(ParserReader r, Collection<E> l, | |
ClassMeta<?> type, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException { | |
int S0=0; // Looking for outermost [ | |
int S1=1; // Looking for starting [ or { or " or ' or LITERAL or ] | |
int S2=2; // Looking for , or ] | |
int S3=3; // Looking for starting [ or { or " or ' or LITERAL | |
int argIndex = 0; | |
int state = S0; | |
int c = 0; | |
while (c != -1) { | |
c = r.read(); | |
if (state == S0) { | |
if (c == '[') | |
state = S1; | |
else if (isCommentOrWhitespace(c)) | |
skipCommentsAndSpace(r.unread()); | |
else | |
break; // Invalid character found. | |
} else if (state == S1) { | |
if (c == ']') { | |
return l; | |
} else if (isCommentOrWhitespace(c)) { | |
skipCommentsAndSpace(r.unread()); | |
} else if (c != -1) { | |
l.add((E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), r.unread(), l, pMeta)); | |
state = S2; | |
} | |
} else if (state == S2) { | |
if (c == ',') { | |
state = S3; | |
} else if (isCommentOrWhitespace(c)) { | |
skipCommentsAndSpace(r.unread()); | |
} else if (c == ']') { | |
return l; | |
} else { | |
break; // Invalid character found. | |
} | |
} else if (state == S3) { | |
if (isCommentOrWhitespace(c)) { | |
skipCommentsAndSpace(r.unread()); | |
} else if (c == ']') { | |
break; | |
} else if (c != -1) { | |
l.add((E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), r.unread(), l, pMeta)); | |
state = S2; | |
} | |
} | |
} | |
if (state == S0) | |
throw new ParseException(this, "Expected '[' at beginning of JSON array."); | |
if (state == S1) | |
throw new ParseException(this, "Expected one of the following characters: {,[,',\",LITERAL."); | |
if (state == S2) | |
throw new ParseException(this, "Expected ',' or ']'."); | |
if (state == S3) | |
throw new ParseException(this, "Unexpected trailing comma in array."); | |
return null; // Unreachable. | |
} | |
private <T> BeanMap<T> parseIntoBeanMap2(ParserReader r, BeanMap<T> m) throws IOException, ParseException, ExecutableException { | |
int S0=0; // Looking for outer { | |
int S1=1; // Looking for attrName start. | |
int S3=3; // Found attrName end, looking for :. | |
int S4=4; // Found :, looking for valStart: { [ " ' LITERAL. | |
int S5=5; // Looking for , or } | |
int state = S0; | |
String currAttr = ""; | |
int c = 0; | |
mark(); | |
try { | |
while (c != -1) { | |
c = r.read(); | |
if (state == S0) { | |
if (c == '{') { | |
state = S1; | |
} else if (isCommentOrWhitespace(c)) { | |
skipCommentsAndSpace(r.unread()); | |
} else { | |
break; | |
} | |
} else if (state == S1) { | |
if (c == '}') { | |
return m; | |
} else if (isCommentOrWhitespace(c)) { | |
skipCommentsAndSpace(r.unread()); | |
} else { | |
r.unread(); | |
mark(); | |
currAttr = parseFieldName(r); | |
state = S3; | |
} | |
} else if (state == S3) { | |
if (c == ':') | |
state = S4; | |
} else if (state == S4) { | |
if (isCommentOrWhitespace(c)) { | |
skipCommentsAndSpace(r.unread()); | |
} else { | |
if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) { | |
BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr); | |
setCurrentProperty(pMeta); | |
if (pMeta == null) { | |
onUnknownProperty(currAttr, m); | |
unmark(); | |
parseAnything(object(), r.unread(), m.getBean(false), null); // Read content anyway to ignore it | |
} else { | |
unmark(); | |
ClassMeta<?> cm = pMeta.getClassMeta(); | |
Object value = parseAnything(cm, r.unread(), m.getBean(false), pMeta); | |
setName(cm, value, currAttr); | |
pMeta.set(m, currAttr, value); | |
} | |
setCurrentProperty(null); | |
} | |
state = S5; | |
} | |
} else if (state == S5) { | |
if (c == ',') | |
state = S1; | |
else if (isCommentOrWhitespace(c)) | |
skipCommentsAndSpace(r.unread()); | |
else if (c == '}') { | |
return m; | |
} | |
} | |
} | |
if (state == S0) | |
throw new ParseException(this, "Expected '{' at beginning of JSON object."); | |
if (state == S1) | |
throw new ParseException(this, "Could not find attribute name on JSON object."); | |
if (state == S3) | |
throw new ParseException(this, "Could not find ':' following attribute name on JSON object."); | |
if (state == S4) | |
throw new ParseException(this, "Expected one of the following characters: {,[,',\",LITERAL."); | |
if (state == S5) | |
throw new ParseException(this, "Could not find '}' marking end of JSON object."); | |
} finally { | |
unmark(); | |
} | |
return null; // Unreachable. | |
} | |
/* | |
* Starting from the specified position in the character array, returns the | |
* position of the character " or '. | |
* If the string consists of a concatenation of strings (e.g. 'AAA' + "BBB"), this method | |
* will automatically concatenate the strings and return the result. | |
*/ | |
private String parseString(ParserReader r) throws IOException, ParseException { | |
r.mark(); | |
int qc = r.read(); // The quote character being used (" or ') | |
if (qc != '"' && isStrict()) { | |
String msg = ( | |
qc == '\'' | |
? "Invalid quote character \"{0}\" being used." | |
: "Did not find quote character marking beginning of string. Character=\"{0}\"" | |
); | |
throw new ParseException(this, msg, (char)qc); | |
} | |
final boolean isQuoted = (qc == '\'' || qc == '"'); | |
String s = null; | |
boolean isInEscape = false; | |
int c = 0; | |
while (c != -1) { | |
c = r.read(); | |
// Strict syntax requires that all control characters be escaped. | |
if (isStrict() && c <= 0x1F) | |
throw new ParseException(this, "Unescaped control character encountered: ''0x{0}''", String.format("%04X", c)); | |
if (isInEscape) { | |
switch (c) { | |
case 'n': r.replace('\n'); break; | |
case 'r': r.replace('\r'); break; | |
case 't': r.replace('\t'); break; | |
case 'f': r.replace('\f'); break; | |
case 'b': r.replace('\b'); break; | |
case '\\': r.replace('\\'); break; | |
case '/': r.replace('/'); break; | |
case '\'': r.replace('\''); break; | |
case '"': r.replace('"'); break; | |
case 'u': { | |
String n = r.read(4); | |
try { | |
r.replace(Integer.parseInt(n, 16), 6); | |
} catch (NumberFormatException e) { | |
throw new ParseException(this, "Invalid Unicode escape sequence in string."); | |
} | |
break; | |
} | |
default: | |
throw new ParseException(this, "Invalid escape sequence in string."); | |
} | |
isInEscape = false; | |
} else { | |
if (c == '\\') { | |
isInEscape = true; | |
r.delete(); | |
} else if (isQuoted) { | |
if (c == qc) { | |
s = r.getMarked(1, -1); | |
break; | |
} | |
} else { | |
if (c == ',' || c == '}' || c == ']' || isWhitespace(c)) { | |
s = r.getMarked(0, -1); | |
r.unread(); | |
break; | |
} else if (c == -1) { | |
s = r.getMarked(0, 0); | |
break; | |
} | |
} | |
} | |
} | |
if (s == null) | |
throw new ParseException(this, "Could not find expected end character ''{0}''.", (char)qc); | |
// Look for concatenated string (i.e. whitespace followed by +). | |
skipCommentsAndSpace(r); | |
if (r.peek() == '+') { | |
if (isStrict()) | |
throw new ParseException(this, "String concatenation detected."); | |
r.read(); // Skip past '+' | |
skipCommentsAndSpace(r); | |
s += parseString(r); | |
} | |
return trim(s); // End of input reached. | |
} | |
/* | |
* Looks for the keywords true, false, or null. | |
* Throws an exception if any of these keywords are not found at the specified position. | |
*/ | |
private void parseKeyword(String keyword, ParserReader r) throws IOException, ParseException { | |
try { | |
String s = r.read(keyword.length()); | |
if (s.equals(keyword)) | |
return; | |
throw new ParseException(this, "Unrecognized syntax. Expected=''{0}'', Actual=''{1}''", keyword, s); | |
} catch (IndexOutOfBoundsException e) { | |
throw new ParseException(this, "Unrecognized syntax. Expected=''{0}'', found end-of-file.", keyword); | |
} | |
} | |
/* | |
* Doesn't actually parse anything, but moves the position beyond any whitespace or comments. | |
* If positionOnNext is 'true', then the cursor will be set to the point immediately after | |
* the comments and whitespace. Otherwise, the cursor will be set to the last position of | |
* the comments and whitespace. | |
*/ | |
private void skipCommentsAndSpace(ParserReader r) throws IOException, ParseException { | |
int c = 0; | |
while ((c = r.read()) != -1) { | |
if (! isWhitespace(c)) { | |
if (c == '/') { | |
if (isStrict()) | |
throw new ParseException(this, "Javascript comment detected."); | |
skipComments(r); | |
} else { | |
r.unread(); | |
return; | |
} | |
} | |
} | |
} | |
/* | |
* Doesn't actually parse anything, but moves the position beyond the construct "{wrapperAttr:" when | |
* the @Json(wrapperAttr) annotation is used on a class. | |
*/ | |
private void skipWrapperAttrStart(ParserReader r, String wrapperAttr) throws IOException, ParseException { | |
int S0=0; // Looking for outer { | |
int S1=1; // Looking for attrName start. | |
int S3=3; // Found attrName end, looking for :. | |
int S4=4; // Found :, looking for valStart: { [ " ' LITERAL. | |
int state = S0; | |
String currAttr = null; | |
int c = 0; | |
while (c != -1) { | |
c = r.read(); | |
if (state == S0) { | |
if (c == '{') | |
state = S1; | |
} else if (state == S1) { | |
if (isCommentOrWhitespace(c)) { | |
skipCommentsAndSpace(r.unread()); | |
} else { | |
currAttr = parseFieldName(r.unread()); | |
if (! currAttr.equals(wrapperAttr)) | |
throw new ParseException(this, | |
"Expected to find wrapper attribute ''{0}'' but found attribute ''{1}''", wrapperAttr, currAttr); | |
state = S3; | |
} | |
} else if (state == S3) { | |
if (c == ':') | |
state = S4; | |
} else if (state == S4) { | |
if (isCommentOrWhitespace(c)) { | |
skipCommentsAndSpace(r.unread()); | |
} else { | |
r.unread(); | |
return; | |
} | |
} | |
} | |
if (state == S0) | |
throw new ParseException(this, "Expected '{' at beginning of JSON object."); | |
if (state == S1) | |
throw new ParseException(this, "Could not find attribute name on JSON object."); | |
if (state == S3) | |
throw new ParseException(this, "Could not find ':' following attribute name on JSON object."); | |
if (state == S4) | |
throw new ParseException(this, "Expected one of the following characters: {,[,',\",LITERAL."); | |
} | |
/* | |
* Doesn't actually parse anything, but moves the position beyond the construct "}" when | |
* the @Json(wrapperAttr) annotation is used on a class. | |
*/ | |
private void skipWrapperAttrEnd(ParserReader r) throws ParseException, IOException { | |
int c = 0; | |
while ((c = r.read()) != -1) { | |
if (! isWhitespace(c)) { | |
if (c == '/') { | |
if (isStrict()) | |
throw new ParseException(this, "Javascript comment detected."); | |
skipComments(r); | |
} else if (c == '}') { | |
return; | |
} else { | |
throw new ParseException(this, "Could not find '}' at the end of JSON wrapper object."); | |
} | |
} | |
} | |
} | |
/* | |
* Doesn't actually parse anything, but when positioned at the beginning of comment, | |
* it will move the pointer to the last character in the comment. | |
*/ | |
private void skipComments(ParserReader r) throws ParseException, IOException { | |
int c = r.read(); | |
// "/* */" style comments | |
if (c == '*') { | |
while (c != -1) | |
if ((c = r.read()) == '*') | |
if ((c = r.read()) == '/') | |
return; | |
// "//" style comments | |
} else if (c == '/') { | |
while (c != -1) { | |
c = r.read(); | |
if (c == -1 || c == '\n') | |
return; | |
} | |
} | |
throw new ParseException(this, "Open ended comment."); | |
} | |
/* | |
* Call this method after you've finished a parsing a string to make sure that if there's any | |
* remainder in the input, that it consists only of whitespace and comments. | |
*/ | |
private void validateEnd(ParserReader r) throws IOException, ParseException { | |
if (! isValidateEnd()) | |
return; | |
skipCommentsAndSpace(r); | |
int c = r.read(); | |
if (c != -1 && c != ';') // var x = {...}; expressions can end with a semicolon. | |
throw new ParseException(this, "Remainder after parse: ''{0}''.", (char)c); | |
} | |
//----------------------------------------------------------------------------------------------------------------- | |
// Properties | |
//----------------------------------------------------------------------------------------------------------------- | |
/** | |
* Configuration property: Validate end. | |
* | |
* @see JsonParser#JSON_validateEnd | |
* @return | |
* <jk>true</jk> if after parsing a POJO from the input, verifies that the remaining input in | |
* the stream consists of only comments or whitespace. | |
*/ | |
protected final boolean isValidateEnd() { | |
return ctx.isValidateEnd(); | |
} | |
//----------------------------------------------------------------------------------------------------------------- | |
// Other methods | |
//----------------------------------------------------------------------------------------------------------------- | |
@Override /* Session */ | |
public ObjectMap toMap() { | |
return super.toMap() | |
.append("JsonParserSession", new DefaultFilteringObjectMap() | |
); | |
} | |
} |