blob: 7cc5e8101082f2b89bb74702bf4a160a59b89dc4 [file] [log] [blame]
// ***************************************************************************************************************************
// * 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.uon;
import static org.apache.juneau.internal.StringUtils.*;
import static org.apache.juneau.uon.UonParser.*;
import java.io.*;
import java.lang.reflect.*;
import java.util.*;
import org.apache.juneau.*;
import org.apache.juneau.httppart.*;
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 UonParser}.
*
* <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 class UonParserSession extends ReaderParserSession implements HttpPartParserSession {
// Characters that need to be preceded with an escape character.
private static final AsciiSet escapedChars = AsciiSet.create("~'\u0001\u0002");
private static final char AMP='\u0001', EQ='\u0002'; // Flags set in reader to denote & and = characters.
private final UonParser ctx;
private final boolean decoding;
/**
* 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 UonParserSession(UonParser ctx, ParserSessionArgs args) {
super(ctx, args);
this.ctx = ctx;
decoding = getProperty(UON_decoding, boolean.class, ctx.isDecoding());
}
/**
* Create a specialized parser session for parsing URL parameters.
*
* <p>
* The main difference is that characters are never decoded, and the {@link UonParser#UON_decoding}
* property is always ignored.
*
* @param ctx
* The context creating this session object.
* The context contains all the configuration settings for this object.
* @param args
* Runtime session arguments.
* @param decoding
* Whether to decode characters.
*/
protected UonParserSession(UonParser ctx, ParserSessionArgs args, boolean decoding) {
super(ctx, args);
this.ctx = ctx;
this.decoding = decoding;
}
@Override /* ParserSession */
protected <T> T doParse(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException {
try (UonReader r = getUonReader(pipe, decoding)) {
T o = parseAnything(type, r, getOuter(), true, 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 Exception {
try (UonReader r = getUonReader(pipe, decoding)) {
m = parseIntoMap(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 Exception {
try (UonReader r = getUonReader(pipe, decoding)) {
c = parseIntoCollection(r, c, (ClassMeta<E>)getClassMeta(elementType), false, null);
validateEnd(r);
return c;
}
}
@Override /* HttpPartParser */
public <T> T parse(HttpPartType partType, HttpPartSchema schema, String in, ClassMeta<T> toType) throws ParseException, SchemaValidationException {
if (in == null)
return null;
if (toType.isString() && in.length() > 0) {
// Shortcut - If we're returning a string and the value doesn't start with "'" or is "null", then
// just return the string since it's a plain value.
// This allows us to bypass the creation of a UonParserSession object.
char x = firstNonWhitespaceChar(in);
if (x != '\'' && x != 'n' && in.indexOf('~') == -1)
return (T)in;
if (x == 'n' && "null".equals(in))
return null;
}
try (ParserPipe pipe = createPipe(in)) {
try (UonReader r = getUonReader(pipe, false)) {
return parseAnything(toType, r, null, true, null);
}
} catch (ParseException e) {
throw e;
} catch (Exception e) {
throw new ParseException(e);
}
}
@Override /* HttpPartParserSession */
public <T> T parse(HttpPartType partType, HttpPartSchema schema, String in, Class<T> toType) throws ParseException, SchemaValidationException {
return parse(null, schema, in, getClassMeta(toType));
}
@Override /* HttpPartParserSession */
public <T> T parse(HttpPartType partType, HttpPartSchema schema, String in, Type toType, Type...toTypeArgs) throws ParseException, SchemaValidationException {
return (T)parse(null, schema, in, getClassMeta(toType, toTypeArgs));
}
@Override /* HttpPartParserSession */
public <T> T parse(HttpPartSchema schema, String in, Class<T> toType) throws ParseException, SchemaValidationException {
return parse(null, schema, in, getClassMeta(toType));
}
@Override /* HttpPartParserSession */
public <T> T parse(HttpPartSchema schema, String in, ClassMeta<T> toType) throws ParseException, SchemaValidationException {
return parse(null, schema, in, toType);
}
@Override /* HttpPartParserSession */
public <T> T parse(HttpPartSchema schema, String in, Type toType, Type...toTypeArgs) throws ParseException, SchemaValidationException {
return (T)parse(null, schema, in, getClassMeta(toType, toTypeArgs));
}
/**
* Workhorse method.
*
* @param <T> The class type being parsed, or <jk>null</jk> if unknown.
* @param eType The class type being parsed, or <jk>null</jk> if unknown.
* @param r The reader being parsed.
* @param outer The outer object (for constructing nested inner classes).
* @param isUrlParamValue
* If <jk>true</jk>, then we're parsing a top-level URL-encoded value which is treated a bit different than the
* default case.
* @param pMeta The current bean property being parsed.
* @return The parsed object.
* @throws IOException Thrown by underlying stream.
* @throws ParseException Malformed input encountered.
* @throws ExecutableException Exception occurred on invoked constructor/method/field.
*/
public <T> T parseAnything(ClassMeta<?> eType, UonReader r, Object outer, boolean isUrlParamValue, 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;
setCurrentClass(sType);
Object o = null;
int c = r.peekSkipWs();
if (c == -1 || c == AMP) {
// If parameter is blank and it's an array or collection, return an empty list.
if (sType.isCollectionOrArray())
o = sType.newInstance();
else if (sType.isString() || sType.isObject())
o = "";
else if (sType.isPrimitive())
o = sType.getPrimitiveDefault();
// Otherwise, leave null.
} else if (sType.isVoid()) {
String s = parseString(r, isUrlParamValue);
if (s != null)
throw new ParseException(this, "Expected ''null'' for void value, but was ''{0}''.", s);
} else if (sType.isObject()) {
if (c == '(') {
ObjectMap m = new ObjectMap(this);
parseIntoMap(r, m, string(), object(), pMeta);
o = cast(m, pMeta, eType);
} else if (c == '@') {
Collection l = new ObjectList(this);
o = parseIntoCollection(r, l, sType, isUrlParamValue, pMeta);
} else {
String s = parseString(r, isUrlParamValue);
if (c != '\'') {
if ("true".equals(s) || "false".equals(s))
o = Boolean.valueOf(s);
else if (! "null".equals(s)) {
if (isNumeric(s))
o = StringUtils.parseNumber(s, Number.class);
else
o = s;
}
} else {
o = s;
}
}
} else if (sType.isBoolean()) {
o = parseBoolean(r);
} else if (sType.isCharSequence()) {
o = parseString(r, isUrlParamValue);
} else if (sType.isChar()) {
o = parseCharacter(parseString(r, isUrlParamValue));
} 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 = parseIntoMap(r, m, sType.getKeyType(), sType.getValueType(), pMeta);
} else if (sType.isCollection()) {
if (c == '(') {
ObjectMap m = new ObjectMap(this);
parseIntoMap(r, m, string(), object(), pMeta);
// Handle case where it's a collection, but serialized as a map with a _type or _value key.
if (m.containsKey(getBeanTypePropertyName(sType)))
o = cast(m, pMeta, eType);
// Handle case where it's a collection, but only a single value was specified.
else {
Collection l = (
sType.canCreateNewInstance(outer)
? (Collection)sType.newInstance(outer)
: new ObjectList(this)
);
l.add(m.cast(sType.getElementType()));
o = l;
}
} else {
Collection l = (
sType.canCreateNewInstance(outer)
? (Collection)sType.newInstance(outer)
: new ObjectList(this)
);
o = parseIntoCollection(r, l, sType, isUrlParamValue, pMeta);
}
} else if (builder != null) {
BeanMap m = toBeanMap(builder.create(this, eType));
m = parseIntoBeanMap(r, m);
o = m == null ? null : builder.build(this, m.getBean(), eType);
} else if (sType.canCreateNewBean(outer)) {
BeanMap m = newBeanMap(outer, sType.getInnerClass());
m = parseIntoBeanMap(r, m);
o = m == null ? null : m.getBean();
} else if (sType.canCreateNewInstanceFromString(outer)) {
String s = parseString(r, isUrlParamValue);
if (s != null)
o = sType.newInstanceFromString(outer, s);
} else if (sType.isArray() || sType.isArgs()) {
if (c == '(') {
ObjectMap m = new ObjectMap(this);
parseIntoMap(r, m, string(), object(), pMeta);
// Handle case where it's an array, but serialized as a map with a _type or _value key.
if (m.containsKey(getBeanTypePropertyName(sType)))
o = cast(m, pMeta, eType);
// Handle case where it's an array, but only a single value was specified.
else {
ArrayList l = new ArrayList(1);
l.add(m.cast(sType.getElementType()));
o = toArray(sType, l);
}
} else {
ArrayList l = (ArrayList)parseIntoCollection(r, new ArrayList(), sType, isUrlParamValue, pMeta);
o = toArray(sType, l);
}
} else if (c == '(') {
// It could be a non-bean with _type attribute.
ObjectMap m = new ObjectMap(this);
parseIntoMap(r, m, string(), object(), pMeta);
if (m.containsKey(getBeanTypePropertyName(sType)))
o = cast(m, pMeta, eType);
else
throw new ParseException(this, "Class ''{0}'' could not be instantiated. Reason: ''{1}''",
sType.getInnerClass().getName(), sType.getNotABeanReason());
} else if (c == 'n') {
r.read();
parseNull(r);
} else {
throw new ParseException(this, "Class ''{0}'' could not be instantiated. Reason: ''{1}''",
sType.getInnerClass().getName(), sType.getNotABeanReason());
}
if (o == null && sType.isPrimitive())
o = sType.getPrimitiveDefault();
if (swap != null && o != null)
o = unswap(swap, o, eType);
if (outer != null)
setParent(eType, o, outer);
return (T)o;
}
private <K,V> Map<K,V> parseIntoMap(UonReader 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 c = r.read();
if (c == -1 || c == AMP)
return null;
if (c == 'n')
return (Map<K,V>)parseNull(r);
if (c != '(')
throw new ParseException(this, "Expected '(' at beginning of object.");
final int S1=1; // Looking for attrName start.
final int S2=2; // Found attrName end, looking for =.
final int S3=3; // Found =, looking for valStart.
final int S4=4; // Looking for , or )
boolean isInEscape = false;
int state = S1;
K currAttr = null;
while (c != -1 && c != AMP) {
c = r.read();
if (! isInEscape) {
if (state == S1) {
if (c == ')')
return m;
if (Character.isWhitespace(c))
skipSpace(r);
else {
r.unread();
Object attr = parseAttr(r, decoding);
currAttr = attr == null ? null : convertAttrToType(m, trim(attr.toString()), keyType);
state = S2;
c = 0; // Avoid isInEscape if c was '\'
}
} else if (state == S2) {
if (c == EQ || c == '=')
state = S3;
else if (c == -1 || c == ',' || c == ')' || c == AMP) {
if (currAttr == null) {
// Value was '%00'
r.unread();
return null;
}
m.put(currAttr, null);
if (c == ')' || c == -1 || c == AMP)
return m;
state = S1;
}
} else if (state == S3) {
if (c == -1 || c == ',' || c == ')' || c == AMP) {
V value = convertAttrToType(m, "", valueType);
m.put(currAttr, value);
if (c == -1 || c == ')' || c == AMP)
return m;
state = S1;
} else {
V value = parseAnything(valueType, r.unread(), m, false, pMeta);
setName(valueType, value, currAttr);
m.put(currAttr, value);
state = S4;
c = 0; // Avoid isInEscape if c was '\'
}
} else if (state == S4) {
if (c == ',')
state = S1;
else if (c == ')' || c == -1 || c == AMP) {
return m;
}
}
}
isInEscape = isInEscape(c, r, isInEscape);
}
if (state == S1)
throw new ParseException(this, "Could not find attribute name on object.");
if (state == S2)
throw new ParseException(this, "Could not find '=' following attribute name on object.");
if (state == S3)
throw new ParseException(this, "Dangling '=' found in object entry");
if (state == S4)
throw new ParseException(this, "Could not find ')' marking end of object.");
return null; // Unreachable.
}
private <E> Collection<E> parseIntoCollection(UonReader r, Collection<E> l, ClassMeta<E> type, boolean isUrlParamValue, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException {
int c = r.readSkipWs();
if (c == -1 || c == AMP)
return null;
if (c == 'n')
return (Collection<E>)parseNull(r);
int argIndex = 0;
// If we're parsing a top-level parameter, we're allowed to have comma-delimited lists outside parenthesis (e.g. "&foo=1,2,3&bar=a,b,c")
// This is not allowed at lower levels since we use comma's as end delimiters.
boolean isInParens = (c == '@');
if (! isInParens) {
if (isUrlParamValue)
r.unread();
else
throw new ParseException(this, "Could not find '(' marking beginning of collection.");
} else {
r.read();
}
if (isInParens) {
final int S1=1; // Looking for starting of first entry.
final int S2=2; // Looking for starting of subsequent entries.
final int S3=3; // Looking for , or ) after first entry.
int state = S1;
while (c != -1 && c != AMP) {
c = r.read();
if (state == S1 || state == S2) {
if (c == ')') {
if (state == S2) {
l.add((E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(),
r.unread(), l, false, pMeta));
r.read();
}
return l;
} else if (Character.isWhitespace(c)) {
skipSpace(r);
} else {
l.add((E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(),
r.unread(), l, false, pMeta));
state = S3;
}
} else if (state == S3) {
if (c == ',') {
state = S2;
} else if (c == ')') {
return l;
}
}
}
if (state == S1 || state == S2)
throw new ParseException(this, "Could not find start of entry in array.");
if (state == S3)
throw new ParseException(this, "Could not find end of entry in array.");
} else {
final int S1=1; // Looking for starting of entry.
final int S2=2; // Looking for , or & or END after first entry.
int state = S1;
while (c != -1 && c != AMP) {
c = r.read();
if (state == S1) {
if (Character.isWhitespace(c)) {
skipSpace(r);
} else {
l.add((E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(),
r.unread(), l, false, pMeta));
state = S2;
}
} else if (state == S2) {
if (c == ',') {
state = S1;
} else if (Character.isWhitespace(c)) {
skipSpace(r);
} else if (c == AMP || c == -1) {
r.unread();
return l;
}
}
}
}
return null; // Unreachable.
}
private <T> BeanMap<T> parseIntoBeanMap(UonReader r, BeanMap<T> m) throws IOException, ParseException, ExecutableException {
int c = r.readSkipWs();
if (c == -1 || c == AMP)
return null;
if (c == 'n')
return (BeanMap<T>)parseNull(r);
if (c != '(')
throw new ParseException(this, "Expected '(' at beginning of object.");
final int S1=1; // Looking for attrName start.
final int S2=2; // Found attrName end, looking for =.
final int S3=3; // Found =, looking for valStart.
final int S4=4; // Looking for , or }
boolean isInEscape = false;
int state = S1;
String currAttr = "";
mark();
try {
while (c != -1 && c != AMP) {
c = r.read();
if (! isInEscape) {
if (state == S1) {
if (c == ')' || c == -1 || c == AMP) {
return m;
}
if (Character.isWhitespace(c))
skipSpace(r);
else {
r.unread();
mark();
currAttr = parseAttrName(r, decoding);
if (currAttr == null) { // Value was '%00'
return null;
}
state = S2;
}
} else if (state == S2) {
if (c == EQ || c == '=')
state = S3;
else if (c == -1 || c == ',' || c == ')' || c == AMP) {
m.put(currAttr, null);
if (c == ')' || c == -1 || c == AMP) {
return m;
}
state = S1;
}
} else if (state == S3) {
if (c == -1 || c == ',' || c == ')' || c == AMP) {
if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) {
BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr);
if (pMeta == null) {
onUnknownProperty(currAttr, m);
unmark();
} else {
unmark();
Object value = convertToType("", pMeta.getClassMeta());
pMeta.set(m, currAttr, value);
}
}
if (c == -1 || c == ')' || c == AMP)
return m;
state = S1;
} else {
if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) {
BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr);
if (pMeta == null) {
onUnknownProperty(currAttr, m);
unmark();
parseAnything(object(), r.unread(), m.getBean(false), false, null); // Read content anyway to ignore it
} else {
unmark();
setCurrentProperty(pMeta);
ClassMeta<?> cm = pMeta.getClassMeta();
Object value = parseAnything(cm, r.unread(), m.getBean(false), false, pMeta);
setName(cm, value, currAttr);
pMeta.set(m, currAttr, value);
setCurrentProperty(null);
}
}
state = S4;
}
} else if (state == S4) {
if (c == ',')
state = S1;
else if (c == ')' || c == -1 || c == AMP) {
return m;
}
}
}
isInEscape = isInEscape(c, r, isInEscape);
}
if (state == S1)
throw new ParseException(this, "Could not find attribute name on object.");
if (state == S2)
throw new ParseException(this, "Could not find '=' following attribute name on object.");
if (state == S3)
throw new ParseException(this, "Could not find value following '=' on object.");
if (state == S4)
throw new ParseException(this, "Could not find ')' marking end of object.");
} finally {
unmark();
}
return null; // Unreachable.
}
private Object parseNull(UonReader r) throws IOException, ParseException {
String s = parseString(r, false);
if ("ull".equals(s))
return null;
throw new ParseException(this, "Unexpected character sequence: ''{0}''", s);
}
/**
* Convenience method for parsing an attribute from the specified parser.
*
* @param r The reader.
* @param encoded Whether the attribute is encoded.
* @return The parsed object
* @throws IOException Exception thrown by underlying stream.
* @throws ParseException Attribute was malformed.
*/
protected final Object parseAttr(UonReader r, boolean encoded) throws IOException, ParseException {
Object attr;
attr = parseAttrName(r, encoded);
return attr;
}
/**
* Parses an attribute name from the specified reader.
*
* @param r The reader.
* @param encoded Whether the attribute is encoded.
* @return The parsed attribute name.
* @throws IOException Exception thrown by underlying stream.
* @throws ParseException Attribute name was malformed.
*/
protected final String parseAttrName(UonReader r, boolean encoded) throws IOException, ParseException {
// If string is of form 'xxx', we're looking for ' at the end.
// Otherwise, we're looking for '&' or '=' or WS or -1 denoting the end of this string.
int c = r.peekSkipWs();
if (c == '\'')
return parsePString(r);
r.mark();
boolean isInEscape = false;
if (encoded) {
while (c != -1) {
c = r.read();
if (! isInEscape) {
if (c == AMP || c == EQ || c == -1 || Character.isWhitespace(c)) {
if (c != -1)
r.unread();
String s = r.getMarked();
return ("null".equals(s) ? null : s);
}
}
else if (c == AMP)
r.replace('&');
else if (c == EQ)
r.replace('=');
isInEscape = isInEscape(c, r, isInEscape);
}
} else {
while (c != -1) {
c = r.read();
if (! isInEscape) {
if (c == '=' || c == -1 || Character.isWhitespace(c)) {
if (c != -1)
r.unread();
String s = r.getMarked();
return ("null".equals(s) ? null : trim(s));
}
}
isInEscape = isInEscape(c, r, isInEscape);
}
}
// We should never get here.
throw new ParseException(this, "Unexpected condition.");
}
/*
* Returns true if the next character in the stream is preceded by an escape '~' character.
*/
private static final boolean isInEscape(int c, ParserReader r, boolean prevIsInEscape) throws IOException {
if (c == '~' && ! prevIsInEscape) {
c = r.peek();
if (escapedChars.contains(c)) {
r.delete();
return true;
}
}
return false;
}
/**
* Parses a string value from the specified reader.
*
* @param r The input reader.
* @param isUrlParamValue Whether this is a URL parameter.
* @return The parsed string.
* @throws IOException Exception thrown by underlying stream.
* @throws ParseException Malformed input found.
*/
protected final String parseString(UonReader r, boolean isUrlParamValue) throws IOException, ParseException {
// If string is of form 'xxx', we're looking for ' at the end.
// Otherwise, we're looking for ',' or ')' or -1 denoting the end of this string.
int c = r.peekSkipWs();
if (c == '\'')
return parsePString(r);
r.mark();
boolean isInEscape = false;
String s = null;
AsciiSet endChars = (isUrlParamValue ? endCharsParam : endCharsNormal);
while (c != -1) {
c = r.read();
if (! isInEscape) {
// If this is a URL parameter value, we're looking for: &
// If not, we're looking for: &,)
if (endChars.contains(c)) {
r.unread();
c = -1;
}
}
if (c == -1)
s = r.getMarked();
else if (c == EQ)
r.replace('=');
else if (Character.isWhitespace(c) && ! isUrlParamValue) {
s = r.getMarked(0, -1);
skipSpace(r);
c = -1;
}
isInEscape = isInEscape(c, r, isInEscape);
}
if (isUrlParamValue)
s = StringUtils.trim(s);
return ("null".equals(s) ? null : trim(s));
}
private static final AsciiSet endCharsParam = AsciiSet.create(""+AMP), endCharsNormal = AsciiSet.create(",)"+AMP);
/*
* Parses a string of the form "'foo'"
* All whitespace within parenthesis are preserved.
*/
private String parsePString(UonReader r) throws IOException, ParseException {
r.read(); // Skip first quote.
r.mark();
int c = 0;
boolean isInEscape = false;
while (c != -1) {
c = r.read();
if (! isInEscape) {
if (c == '\'')
return trim(r.getMarked(0, -1));
}
if (c == EQ)
r.replace('=');
isInEscape = isInEscape(c, r, isInEscape);
}
throw new ParseException(this, "Unmatched parenthesis");
}
private Boolean parseBoolean(UonReader r) throws IOException, ParseException {
String s = parseString(r, false);
if (s == null || s.equals("null"))
return null;
if (s.equalsIgnoreCase("true"))
return true;
if (s.equalsIgnoreCase("false"))
return false;
throw new ParseException(this, "Unrecognized syntax for boolean. ''{0}''.", s);
}
private Number parseNumber(UonReader r, Class<? extends Number> c) throws IOException, ParseException {
String s = parseString(r, false);
if (s == null)
return null;
return StringUtils.parseNumber(s, c);
}
/*
* 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(UonReader r) throws IOException, ParseException {
if (! isValidateEnd())
return;
while (true) {
int c = r.read();
if (c == -1)
return;
if (! Character.isWhitespace(c))
throw new ParseException(this, "Remainder after parse: ''{0}''.", (char)c);
}
}
private static void skipSpace(ParserReader r) throws IOException {
int c = 0;
while ((c = r.read()) != -1) {
if (c <= 2 || ! Character.isWhitespace(c)) {
r.unread();
return;
}
}
}
/**
* Creates a {@link UonReader} from the specified parser pipe.
*
* @param pipe The parser input.
* @param decodeChars Whether the reader should automatically decode URL-encoded characters.
* @return A new {@link UonReader} object.
* @throws IOException Thrown by underlying stream.
*/
public final UonReader getUonReader(ParserPipe pipe, boolean decodeChars) throws IOException {
Reader r = pipe.getReader();
if (r instanceof UonReader)
return (UonReader)r;
return new UonReader(pipe, decodeChars);
}
//-----------------------------------------------------------------------------------------------------------------
// Properties
//-----------------------------------------------------------------------------------------------------------------
/**
* Configuration property: Decode <js>"%xx"</js> sequences.
*
* @see UonParser#UON_decoding
* @return
* <jk>true</jk> if URI encoded characters should be decoded, <jk>false</jk> if they've already been decoded
* before being passed to this parser.
*/
protected final boolean isDecoding() {
return decoding;
}
/**
* Configuration property: Validate end.
*
* @see UonParser#UON_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("UonParserSession", new DefaultFilteringObjectMap()
.append("decoding", decoding)
);
}
}