http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/2a37f310/juneau-core/src/main/java/org/apache/juneau/uon/UonParserSession.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/uon/UonParserSession.java b/juneau-core/src/main/java/org/apache/juneau/uon/UonParserSession.java index 6fe5ef6..7d50309 100644 --- a/juneau-core/src/main/java/org/apache/juneau/uon/UonParserSession.java +++ b/juneau-core/src/main/java/org/apache/juneau/uon/UonParserSession.java @@ -12,6 +12,7 @@ // *************************************************************************************************************************** package org.apache.juneau.uon; +import static org.apache.juneau.internal.StringUtils.*; import static org.apache.juneau.uon.UonParserContext.*; import java.io.*; @@ -19,19 +20,27 @@ import java.lang.reflect.*; import java.util.*; import org.apache.juneau.*; -import org.apache.juneau.http.*; +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 meant to be discarded after one-time use. + * This class is NOT thread safe. + * It is typically discarded after one-time use although it can be reused against multiple inputs. */ -public class UonParserSession extends ParserSession { +@SuppressWarnings({ "unchecked", "rawtypes" }) +public class UonParserSession extends ReaderParserSession { + + // Characters that need to be preceded with an escape character. + private static final AsciiSet escapedChars = new AsciiSet("~'\u0001\u0002"); + + private static final char AMP='\u0001', EQ='\u0002'; // Flags set in reader to denote & and = characters. + private final boolean decodeChars; - private UonReader reader; /** * Create a new session using properties specified in the context. @@ -39,36 +48,16 @@ public class UonParserSession extends ParserSession { * @param ctx * The context creating this session object. * The context contains all the configuration settings for this object. - * @param input - * The input. - * Can be any of the following types: - * <ul> - * <li><jk>null</jk> - * <li>{@link Reader} - * <li>{@link CharSequence} - * <li>{@link InputStream} containing UTF-8 encoded text. - * <li>{@link File} containing system encoded text. - * </ul> - * @param op - * The override properties. - * These override any context properties defined in the context. - * @param javaMethod The java method that called this parser, usually the method in a REST servlet. - * @param outer The outer object for instantiating top-level non-static inner classes. - * @param locale - * The session locale. - * If <jk>null</jk>, then the locale defined on the context is used. - * @param timeZone - * The session timezone. - * If <jk>null</jk>, then the timezone defined on the context is used. - * @param mediaType The session media type (e.g. <js>"application/json"</js>). + * @param args + * Runtime session arguments. */ - public UonParserSession(UonParserContext ctx, ObjectMap op, Object input, Method javaMethod, Object outer, - Locale locale, TimeZone timeZone, MediaType mediaType) { - super(ctx, op, input, javaMethod, outer, locale, timeZone, mediaType); - if (op == null || op.isEmpty()) { + protected UonParserSession(UonParserContext ctx, ParserSessionArgs args) { + super(ctx, args); + ObjectMap p = getProperties(); + if (p.isEmpty()) { decodeChars = ctx.decodeChars; } else { - decodeChars = op.getBoolean(UON_decodeChars, ctx.decodeChars); + decodeChars = p.getBoolean(UON_decodeChars, ctx.decodeChars); } } @@ -80,52 +69,695 @@ public class UonParserSession extends ParserSession { * property is always ignored. * * @param ctx The context to copy setting from. - * @param input - * The input. - * Can be any of the following types: - * <ul> - * <li><jk>null</jk> - * <li>{@link Reader} - * <li>{@link CharSequence} (e.g. {@link String}) - * <li>{@link InputStream} - Read as UTF-8 encoded character stream. - * <li>{@link File} - Read as system-default encoded stream. - * </ul> */ - public UonParserSession(UonParserContext ctx, Object input) { - super(ctx, null, input, null, null, null, null, null); + protected UonParserSession(UonParserContext ctx) { + super(ctx, null); decodeChars = false; } + @Override /* ParserSession */ + protected <T> T doParse(ParserPipe pipe, ClassMeta<T> type) throws Exception { + UonReader r = getUonReader(pipe, decodeChars); + 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 { + UonReader r = getUonReader(pipe, decodeChars); + 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 { + UonReader r = getUonReader(pipe, decodeChars); + c = parseIntoCollection(r, c, (ClassMeta<E>)getClassMeta(elementType), false, null); + validateEnd(r); + return c; + } + /** - * Returns the {@link UonParserContext#UON_decodeChars} setting value for this session. + * Workhorse method. * - * @return The {@link UonParserContext#UON_decodeChars} setting value for this session. + * @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 Exception */ - public final boolean isDecodeChars() { - return decodeChars; + public <T> T parseAnything(ClassMeta<T> eType, UonReader r, Object outer, boolean isUrlParamValue, BeanPropertyMeta pMeta) throws Exception { + + if (eType == null) + eType = (ClassMeta<T>)object(); + PojoSwap<T,Object> transform = (PojoSwap<T,Object>)eType.getPojoSwap(); + ClassMeta<?> sType = eType.getSerializedClassMeta(); + + 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(loc(r), "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()) { + String s = parseString(r, isUrlParamValue); + o = s == null ? null : s.charAt(0); + } 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 (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.canCreateNewInstanceFromNumber(outer)) { + o = sType.newInstanceFromNumber(this, outer, parseNumber(r, sType.getNewInstanceFromNumberClass())); + } 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(loc(r), "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(loc(r), "Class ''{0}'' could not be instantiated. Reason: ''{1}''", + sType.getInnerClass().getName(), sType.getNotABeanReason()); + } + + if (o == null && sType.isPrimitive()) + o = sType.getPrimitiveDefault(); + if (transform != null && o != null) + o = transform.unswap(this, o, eType); + + if (outer != null) + setParent(eType, o, outer); + + return (T)o; } - @Override /* ParserSession */ - public UonReader getReader() throws Exception { - if (reader == null) { - Object input = getInput(); - if (input instanceof UonReader) - reader = (UonReader)input; - else if (input instanceof CharSequence) - reader = new UonReader((CharSequence)input, decodeChars); + private <K,V> Map<K,V> parseIntoMap(UonReader r, Map<K,V> m, ClassMeta<K> keyType, ClassMeta<V> valueType, + BeanPropertyMeta pMeta) throws Exception { + + 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(loc(r), "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, decodeChars); + 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(loc(r), "Could not find attribute name on object."); + if (state == S2) + throw new ParseException(loc(r), "Could not find '=' following attribute name on object."); + if (state == S3) + throw new ParseException(loc(r), "Dangling '=' found in object entry"); + if (state == S4) + throw new ParseException(loc(r), "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 Exception { + + 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 - reader = new UonReader(super.getReader(), decodeChars); + throw new ParseException(loc(r), "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(loc(r), "Could not find start of entry in array."); + if (state == S3) + throw new ParseException(loc(r), "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 reader; + + return null; // Unreachable. } - @Override /* ParserSession */ - public Map<String,Object> getLastLocation() { - Map<String,Object> m = super.getLastLocation(); - if (reader != null) { - m.put("line", reader.getLine()); - m.put("column", reader.getColumn()); + private <T> BeanMap<T> parseIntoBeanMap(UonReader r, BeanMap<T> m) throws Exception { + + int c = r.readSkipWs(); + if (c == -1 || c == AMP) + return null; + if (c == 'n') + return (BeanMap<T>)parseNull(r); + if (c != '(') + throw new ParseException(loc(r), "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 = ""; + int currAttrLine = -1, currAttrCol = -1; + 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(); + currAttrLine= r.getLine(); + currAttrCol = r.getColumn(); + currAttr = parseAttrName(r, decodeChars); + 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(r.getPipe(), currAttr, m, currAttrLine, currAttrCol); + } else { + 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(r.getPipe(), currAttr, m, currAttrLine, currAttrCol); + parseAnything(object(), r.unread(), m.getBean(false), false, null); // Read content anyway to ignore it + } else { + 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); } - return m; + if (state == S1) + throw new ParseException(loc(r), "Could not find attribute name on object."); + if (state == S2) + throw new ParseException(loc(r), "Could not find '=' following attribute name on object."); + if (state == S3) + throw new ParseException(loc(r), "Could not find value following '=' on object."); + if (state == S4) + throw new ParseException(loc(r), "Could not find ')' marking end of object."); + + return null; // Unreachable. + } + + private Object parseNull(UonReader r) throws Exception { + String s = parseString(r, false); + if ("ull".equals(s)) + return null; + throw new ParseException(loc(r), "Unexpected character sequence: ''{0}''", s); + } + + /** + * Convenience method for parsing an attribute from the specified parser. + * + * @param r + * @param encoded + * @return The parsed object + * @throws Exception + */ + protected final Object parseAttr(UonReader r, boolean encoded) throws Exception { + Object attr; + attr = parseAttrName(r, encoded); + return attr; + } + + /** + * Parses an attribute name from the specified reader. + * + * @param r + * @param encoded + * @return The parsed attribute name. + * @throws Exception + */ + protected final String parseAttrName(UonReader r, boolean encoded) throws Exception { + + // 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(loc(r), "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 Exception { + 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 + * @param isUrlParamValue + * @return The parsed string. + * @throws Exception + */ + protected final String parseString(UonReader r, boolean isUrlParamValue) throws Exception { + + // 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 = new AsciiSet(""+AMP), endCharsNormal = new AsciiSet(",)"+AMP); + + + /* + * Parses a string of the form "'foo'" + * All whitespace within parenthesis are preserved. + */ + private String parsePString(UonReader r) throws Exception { + + 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(loc(r), "Unmatched parenthesis"); + } + + private Boolean parseBoolean(UonReader r) throws Exception { + String s = parseString(r, false); + if (s == null || s.equals("null")) + return null; + if (s.equals("true")) + return true; + if (s.equals("false")) + return false; + throw new ParseException(loc(r), "Unrecognized syntax for boolean. ''{0}''.", s); + } + + private Number parseNumber(UonReader r, Class<? extends Number> c) throws Exception { + 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 Exception { + while (true) { + int c = r.read(); + if (c == -1) + return; + if (! Character.isWhitespace(c)) + throw new ParseException(loc(r), "Remainder after parse: ''{0}''.", (char)c); + } + } + + private static void skipSpace(ParserReader r) throws Exception { + int c = 0; + while ((c = r.read()) != -1) { + if (c <= 2 || ! Character.isWhitespace(c)) { + r.unread(); + return; + } + } + } + + /** + * Returns a map identifying the current parse location. + * + * @param r The reader being read from. + * @return A map identifying the current parse location. + */ + protected final ObjectMap loc(UonReader r) { + return getLastLocation().append("line", r.getLine()).append("column", r.getColumn()); + } + + /** + * 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 Exception + */ + @SuppressWarnings({ "static-method", "hiding" }) + public final UonReader getUonReader(ParserPipe pipe, boolean decodeChars) throws Exception { + Reader r = pipe.getReader(); + if (r instanceof UonReader) + return (UonReader)r; + return new UonReader(pipe, decodeChars); } }
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/2a37f310/juneau-core/src/main/java/org/apache/juneau/uon/UonReader.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/uon/UonReader.java b/juneau-core/src/main/java/org/apache/juneau/uon/UonReader.java index 2f932bb..3037ad3 100644 --- a/juneau-core/src/main/java/org/apache/juneau/uon/UonReader.java +++ b/juneau-core/src/main/java/org/apache/juneau/uon/UonReader.java @@ -33,33 +33,27 @@ public final class UonReader extends ParserReader { private final boolean decodeChars; private final char[] buff; + + // Writable properties. private int iCurrent, iEnd; - /** - * Constructor for input from a {@link CharSequence}. - * - * @param in The character sequence being read from. - * @param decodeChars If <jk>true</jk>, decode <code>%xx</code> escape sequences. - */ - public UonReader(CharSequence in, boolean decodeChars) { - super(in); - this.decodeChars = decodeChars; - if (in == null || ! decodeChars) - this.buff = new char[0]; - else - this.buff = new char[in.length() < 1024 ? in.length() : 1024]; - } /** - * Constructor for input from a {@link Reader}). + * Constructor. * - * @param r The Reader being wrapped. - * @param decodeChars If <jk>true</jk>, decode <code>%xx</code> escape sequences. + * @param pipe The parser input. + * @param decodeChars Whether the input is URL-encoded. + * @throws Exception */ - public UonReader(Reader r, boolean decodeChars) { - super(r); + public UonReader(ParserPipe pipe, boolean decodeChars) throws Exception { + super(pipe); this.decodeChars = decodeChars; - this.buff = new char[1024]; + if (pipe.isString()) { + String in = pipe.getInputAsString(); + this.buff = new char[in.length() < 1024 ? in.length() : 1024]; + } else { + this.buff = new char[1024]; + } } @Override /* Reader */ @@ -158,7 +152,7 @@ public final class UonReader extends ParserReader { return i; } - private final int readUTF8(int n, final int numBytes) throws IOException { + private int readUTF8(int n, final int numBytes) throws IOException { if (iCurrent + numBytes*3 > iEnd) return -1; for (int i = 0; i < numBytes; i++) { @@ -168,14 +162,14 @@ public final class UonReader extends ParserReader { return n; } - private final int readHex() throws IOException { + private int readHex() throws IOException { int c = buff[iCurrent++]; if (c != '%') throw new IOException("Did not find expected '%' character in UTF-8 sequence."); return readEncodedByte(); } - private final int readEncodedByte() throws IOException { + private int readEncodedByte() throws IOException { if (iEnd <= iCurrent + 1) throw new IOException("Incomplete trailing escape pattern"); int h = buff[iCurrent++]; @@ -185,7 +179,7 @@ public final class UonReader extends ParserReader { return (h << 4) + l; } - private static final int fromHexChar(int c) throws IOException { + private static int fromHexChar(int c) throws IOException { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') @@ -194,4 +188,10 @@ public final class UonReader extends ParserReader { return 10 + c - 'A'; throw new IOException("Invalid hex character '"+c+"' found in escape pattern."); } + + @Override /* ParserReader */ + public final UonReader unread() throws IOException { + super.unread(); + return this; + } } http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/2a37f310/juneau-core/src/main/java/org/apache/juneau/uon/UonSerializer.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/uon/UonSerializer.java b/juneau-core/src/main/java/org/apache/juneau/uon/UonSerializer.java index 7d5c100..5f1dc23 100644 --- a/juneau-core/src/main/java/org/apache/juneau/uon/UonSerializer.java +++ b/juneau-core/src/main/java/org/apache/juneau/uon/UonSerializer.java @@ -15,14 +15,9 @@ package org.apache.juneau.uon; import static org.apache.juneau.serializer.SerializerContext.*; import static org.apache.juneau.uon.UonSerializerContext.*; -import java.lang.reflect.*; -import java.util.*; - import org.apache.juneau.*; import org.apache.juneau.annotation.*; -import org.apache.juneau.http.*; import org.apache.juneau.serializer.*; -import org.apache.juneau.transform.*; /** * Serializes POJO models to UON (a notation for URL-encoded query parameter values). @@ -153,12 +148,7 @@ public class UonSerializer extends WriterSerializer { * @param propertyStore The property store containing all the settings for this object. */ public Readable(PropertyStore propertyStore) { - super(propertyStore); - } - - @Override /* CoreObject */ - protected ObjectMap getOverrideProperties() { - return super.getOverrideProperties().append(SERIALIZER_useWhitespace, true); + super(propertyStore.copy().append(SERIALIZER_useWhitespace, true)); } } @@ -173,12 +163,7 @@ public class UonSerializer extends WriterSerializer { * @param propertyStore The property store containing all the settings for this object. */ public Encoding(PropertyStore propertyStore) { - super(propertyStore); - } - - @Override /* CoreObject */ - protected ObjectMap getOverrideProperties() { - return super.getOverrideProperties().append(UON_encodeChars, true); + super(propertyStore.copy().append(UON_encodeChars, true)); } } @@ -200,189 +185,6 @@ public class UonSerializer extends WriterSerializer { return new UonSerializerBuilder(propertyStore); } - /** - * Workhorse method. Determines the type of object, and then calls the appropriate type-specific serialization - * method. - * - * @param session The context that exist for the duration of a serialize. - * @param out The writer to serialize to. - * @param o The object being serialized. - * @param eType The expected type of the object if this is a bean property. - * @param attrName - * The bean property name if this is a bean property. - * <jk>null</jk> if this isn't a bean property being serialized. - * @param pMeta The bean property metadata. - * @return The same writer passed in. - * @throws Exception - */ - @SuppressWarnings({ "rawtypes", "unchecked" }) - protected SerializerWriter serializeAnything(UonSerializerSession session, UonWriter out, Object o, ClassMeta<?> eType, - String attrName, BeanPropertyMeta pMeta) throws Exception { - - if (o == null) { - out.appendObject(null, false); - return out; - } - - if (eType == null) - eType = object(); - - ClassMeta<?> aType; // The actual type - ClassMeta<?> sType; // The serialized type - - aType = session.push(attrName, o, eType); - boolean isRecursion = aType == null; - - // Handle recursion - if (aType == null) { - o = null; - aType = object(); - } - - sType = aType.getSerializedClassMeta(); - String typeName = session.getBeanTypeName(eType, aType, pMeta); - - // Swap if necessary - PojoSwap swap = aType.getPojoSwap(); - if (swap != null) { - o = swap.swap(session, o); - - // If the getSwapClass() method returns Object, we need to figure out - // the actual type now. - if (sType.isObject()) - sType = session.getClassMetaForObject(o); - } - - // '\0' characters are considered null. - if (o == null || (sType.isChar() && ((Character)o).charValue() == 0)) - out.appendObject(null, false); - else if (sType.isBoolean()) - out.appendBoolean(o); - else if (sType.isNumber()) - out.appendNumber(o); - else if (sType.isBean()) - serializeBeanMap(session, out, session.toBeanMap(o), typeName); - else if (sType.isUri() || (pMeta != null && pMeta.isUri())) - out.appendUri(o); - else if (sType.isMap()) { - if (o instanceof BeanMap) - serializeBeanMap(session, out, (BeanMap)o, typeName); - else - serializeMap(session, out, (Map)o, eType); - } - else if (sType.isCollection()) { - serializeCollection(session, out, (Collection) o, eType); - } - else if (sType.isArray()) { - serializeCollection(session, out, toList(sType.getInnerClass(), o), eType); - } - else { - out.appendObject(o, false); - } - - if (! isRecursion) - session.pop(); - return out; - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - private SerializerWriter serializeMap(UonSerializerSession session, UonWriter out, Map m, ClassMeta<?> type) throws Exception { - - m = session.sort(m); - - ClassMeta<?> keyType = type.getKeyType(), valueType = type.getValueType(); - - int depth = session.getIndent(); - - if (! session.isPlainTextParams()) - out.append('('); - - Iterator mapEntries = m.entrySet().iterator(); - - while (mapEntries.hasNext()) { - Map.Entry e = (Map.Entry) mapEntries.next(); - Object value = e.getValue(); - Object key = session.generalize(e.getKey(), keyType); - out.cr(depth).appendObject(key, false).append('='); - serializeAnything(session, out, value, valueType, (key == null ? null : session.toString(key)), null); - if (mapEntries.hasNext()) - out.append(','); - } - - if (m.size() > 0) - out.cre(depth-1); - - if (! session.isPlainTextParams()) - out.append(')'); - - return out; - } - - private SerializerWriter serializeBeanMap(UonSerializerSession session, UonWriter out, BeanMap<?> m, String typeName) throws Exception { - int depth = session.getIndent(); - - if (! session.isPlainTextParams()) - out.append('('); - - boolean addComma = false; - - for (BeanPropertyValue p : m.getValues(session.isTrimNulls(), typeName != null ? session.createBeanTypeNameProperty(m, typeName) : null)) { - BeanPropertyMeta pMeta = p.getMeta(); - ClassMeta<?> cMeta = p.getClassMeta(); - - String key = p.getName(); - Object value = p.getValue(); - Throwable t = p.getThrown(); - if (t != null) - session.onBeanGetterException(pMeta, t); - - if (session.canIgnoreValue(cMeta, key, value)) - continue; - - if (addComma) - out.append(','); - - out.cr(depth).appendObject(key, false).append('='); - - serializeAnything(session, out, value, cMeta, key, pMeta); - - addComma = true; - } - - if (m.size() > 0) - out.cre(depth-1); - if (! session.isPlainTextParams()) - out.append(')'); - - return out; - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - private SerializerWriter serializeCollection(UonSerializerSession session, UonWriter out, Collection c, ClassMeta<?> type) throws Exception { - - ClassMeta<?> elementType = type.getElementType(); - - c = session.sort(c); - - if (! session.isPlainTextParams()) - out.append('@').append('('); - - int depth = session.getIndent(); - - for (Iterator i = c.iterator(); i.hasNext();) { - out.cr(depth); - serializeAnything(session, out, i.next(), elementType, "<iterator>", null); - if (i.hasNext()) - out.append(','); - } - - if (c.size() > 0) - out.cre(depth-1); - if (! session.isPlainTextParams()) - out.append(')'); - - return out; - } //-------------------------------------------------------------------------------- @@ -390,14 +192,7 @@ public class UonSerializer extends WriterSerializer { //-------------------------------------------------------------------------------- @Override /* Serializer */ - public UonSerializerSession createSession(ObjectMap op, Method javaMethod, Locale locale, - TimeZone timeZone, MediaType mediaType, UriContext uriContext) { - return new UonSerializerSession(ctx, null, op, javaMethod, locale, timeZone, mediaType, uriContext); - } - - @Override /* Serializer */ - protected void doSerialize(SerializerSession session, SerializerOutput out, Object o) throws Exception { - UonSerializerSession s = (UonSerializerSession)session; - serializeAnything(s, s.getUonWriter(out), o, s.getExpectedRootType(o), "root", null); + public WriterSerializerSession createSession(SerializerSessionArgs args) { + return new UonSerializerSession(ctx, null, args); } } http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/2a37f310/juneau-core/src/main/java/org/apache/juneau/uon/UonSerializerSession.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/uon/UonSerializerSession.java b/juneau-core/src/main/java/org/apache/juneau/uon/UonSerializerSession.java index 67c50c5..34039bc 100644 --- a/juneau-core/src/main/java/org/apache/juneau/uon/UonSerializerSession.java +++ b/juneau-core/src/main/java/org/apache/juneau/uon/UonSerializerSession.java @@ -15,20 +15,20 @@ package org.apache.juneau.uon; import static org.apache.juneau.msgpack.MsgPackSerializerContext.*; import static org.apache.juneau.uon.UonSerializerContext.*; -import java.lang.reflect.*; import java.util.*; import org.apache.juneau.*; -import org.apache.juneau.http.*; import org.apache.juneau.serializer.*; +import org.apache.juneau.transform.*; /** * Session object that lives for the duration of a single use of {@link UonSerializer}. * * <p> - * This class is NOT thread safe. It is meant to be discarded after one-time use. + * This class is NOT thread safe. + * It is typically discarded after one-time use although it can be reused within the same thread. */ -public class UonSerializerSession extends SerializerSession { +public class UonSerializerSession extends WriterSerializerSession { private final boolean encodeChars, @@ -36,51 +36,32 @@ public class UonSerializerSession extends SerializerSession { plainTextParams; /** - * 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 encode Override the {@link UonSerializerContext#UON_encodeChars} setting. - * @param op - * The override properties. - * These override any context properties defined in the context. - * @param javaMethod The java method that called this serializer, usually the method in a REST servlet. - * @param locale - * The session locale. - * If <jk>null</jk>, then the locale defined on the context is used. - * @param timeZone - * The session timezone. - * If <jk>null</jk>, then the timezone defined on the context is used. - * @param mediaType The session media type (e.g. <js>"application/json"</js>). - * @param uriContext - * The URI context. - * Identifies the current request URI used for resolution of URIs to absolute or root-relative form. + * @param args + * Runtime arguments. + * These specify session-level information such as locale and URI context. + * It also include session-level properties that override the properties defined on the bean and + * serializer contexts. + * <br>If <jk>null</jk>, defaults to {@link SerializerSessionArgs#DEFAULT}. */ - protected UonSerializerSession(UonSerializerContext ctx, Boolean encode, ObjectMap op, - Method javaMethod, Locale locale, TimeZone timeZone, MediaType mediaType, UriContext uriContext) { - super(ctx, op, javaMethod, locale, timeZone, mediaType, uriContext); - if (op == null || op.isEmpty()) { + public UonSerializerSession(UonSerializerContext ctx, Boolean encode, SerializerSessionArgs args) { + super(ctx, args); + ObjectMap p = getProperties(); + if (p.isEmpty()) { encodeChars = encode == null ? ctx.encodeChars : encode; addBeanTypeProperties = ctx.addBeanTypeProperties; plainTextParams = ctx.plainTextParams; } else { - encodeChars = encode == null ? op.getBoolean(UON_encodeChars, ctx.encodeChars) : encode; - addBeanTypeProperties = op.getBoolean(MSGPACK_addBeanTypeProperties, ctx.addBeanTypeProperties); - plainTextParams = op.getString(UonSerializerContext.UON_paramFormat, "UON").equals("PLAINTEXT"); + encodeChars = encode == null ? p.getBoolean(UON_encodeChars, ctx.encodeChars) : encode; + addBeanTypeProperties = p.getBoolean(MSGPACK_addBeanTypeProperties, ctx.addBeanTypeProperties); + plainTextParams = p.getString(UonSerializerContext.UON_paramFormat, "UON").equals("PLAINTEXT"); } } /** - * Returns the {@link UonSerializerContext#UON_encodeChars} setting value for this session. - * - * @return The {@link UonSerializerContext#UON_encodeChars} setting value for this session. - */ - public final boolean isEncodeChars() { - return encodeChars; - } - - /** * Returns the {@link UonSerializerContext#UON_addBeanTypeProperties} setting value for this session. * * @return The {@link UonSerializerContext#UON_addBeanTypeProperties} setting value for this session. @@ -91,25 +72,200 @@ public class UonSerializerSession extends SerializerSession { } /** - * Returns <jk>true</jk> if the {@link UonSerializerContext#UON_paramFormat} is <js>"PLAINTEXT"</js>. - * - * @return <jk>true</jk> if the {@link UonSerializerContext#UON_paramFormat} is <js>"PLAINTEXT"</js>. - */ - public boolean isPlainTextParams() { - return plainTextParams; - } - - /** * Converts the specified output target object to an {@link UonWriter}. * * @param out The output target object. * @return The output target object wrapped in an {@link UonWriter}. * @throws Exception */ - public final UonWriter getUonWriter(SerializerOutput out) throws Exception { + protected final UonWriter getUonWriter(SerializerPipe out) throws Exception { Object output = out.getRawOutput(); if (output instanceof UonWriter) return (UonWriter)output; - return new UonWriter(this, out.getWriter(), isUseWhitespace(), getMaxIndent(), isEncodeChars(), isTrimStrings(), isPlainTextParams(), getUriResolver()); + UonWriter w = new UonWriter(this, out.getWriter(), isUseWhitespace(), getMaxIndent(), encodeChars, isTrimStrings(), plainTextParams, getUriResolver()); + out.setWriter(w); + return w; + } + + @Override /* Serializer */ + protected void doSerialize(SerializerPipe out, Object o) throws Exception { + serializeAnything(getUonWriter(out), o, getExpectedRootType(o), "root", null); + } + + /** + * Workhorse method. Determines the type of object, and then calls the appropriate type-specific serialization + * method. + * + * @param out The writer to serialize to. + * @param o The object being serialized. + * @param eType The expected type of the object if this is a bean property. + * @param attrName + * The bean property name if this is a bean property. + * <jk>null</jk> if this isn't a bean property being serialized. + * @param pMeta The bean property metadata. + * @return The same writer passed in. + * @throws Exception + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + protected SerializerWriter serializeAnything(UonWriter out, Object o, ClassMeta<?> eType, String attrName, BeanPropertyMeta pMeta) throws Exception { + + if (o == null) { + out.appendObject(null, false); + return out; + } + + if (eType == null) + eType = object(); + + ClassMeta<?> aType; // The actual type + ClassMeta<?> sType; // The serialized type + + aType = push(attrName, o, eType); + boolean isRecursion = aType == null; + + // Handle recursion + if (aType == null) { + o = null; + aType = object(); + } + + sType = aType.getSerializedClassMeta(); + String typeName = getBeanTypeName(eType, aType, pMeta); + + // Swap if necessary + PojoSwap swap = aType.getPojoSwap(); + if (swap != null) { + o = swap.swap(this, o); + + // If the getSwapClass() method returns Object, we need to figure out + // the actual type now. + if (sType.isObject()) + sType = getClassMetaForObject(o); + } + + // '\0' characters are considered null. + if (o == null || (sType.isChar() && ((Character)o).charValue() == 0)) + out.appendObject(null, false); + else if (sType.isBoolean()) + out.appendBoolean(o); + else if (sType.isNumber()) + out.appendNumber(o); + else if (sType.isBean()) + serializeBeanMap(out, toBeanMap(o), typeName); + else if (sType.isUri() || (pMeta != null && pMeta.isUri())) + out.appendUri(o); + else if (sType.isMap()) { + if (o instanceof BeanMap) + serializeBeanMap(out, (BeanMap)o, typeName); + else + serializeMap(out, (Map)o, eType); + } + else if (sType.isCollection()) { + serializeCollection(out, (Collection) o, eType); + } + else if (sType.isArray()) { + serializeCollection(out, toList(sType.getInnerClass(), o), eType); + } + else { + out.appendObject(o, false); + } + + if (! isRecursion) + pop(); + return out; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private SerializerWriter serializeMap(UonWriter out, Map m, ClassMeta<?> type) throws Exception { + + m = sort(m); + + ClassMeta<?> keyType = type.getKeyType(), valueType = type.getValueType(); + + if (! plainTextParams) + out.append('('); + + Iterator mapEntries = m.entrySet().iterator(); + + while (mapEntries.hasNext()) { + Map.Entry e = (Map.Entry) mapEntries.next(); + Object value = e.getValue(); + Object key = generalize(e.getKey(), keyType); + out.cr(indent).appendObject(key, false).append('='); + serializeAnything(out, value, valueType, (key == null ? null : toString(key)), null); + if (mapEntries.hasNext()) + out.append(','); + } + + if (m.size() > 0) + out.cre(indent-1); + + if (! plainTextParams) + out.append(')'); + + return out; + } + + private SerializerWriter serializeBeanMap(UonWriter out, BeanMap<?> m, String typeName) throws Exception { + + if (! plainTextParams) + out.append('('); + + boolean addComma = false; + + for (BeanPropertyValue p : m.getValues(isTrimNulls(), typeName != null ? createBeanTypeNameProperty(m, typeName) : null)) { + BeanPropertyMeta pMeta = p.getMeta(); + ClassMeta<?> cMeta = p.getClassMeta(); + + String key = p.getName(); + Object value = p.getValue(); + Throwable t = p.getThrown(); + if (t != null) + onBeanGetterException(pMeta, t); + + if (canIgnoreValue(cMeta, key, value)) + continue; + + if (addComma) + out.append(','); + + out.cr(indent).appendObject(key, false).append('='); + + serializeAnything(out, value, cMeta, key, pMeta); + + addComma = true; + } + + if (m.size() > 0) + out.cre(indent-1); + if (! plainTextParams) + out.append(')'); + + return out; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private SerializerWriter serializeCollection(UonWriter out, Collection c, ClassMeta<?> type) throws Exception { + + ClassMeta<?> elementType = type.getElementType(); + + c = sort(c); + + if (! plainTextParams) + out.append('@').append('('); + + for (Iterator i = c.iterator(); i.hasNext();) { + out.cr(indent); + serializeAnything(out, i.next(), elementType, "<iterator>", null); + if (i.hasNext()) + out.append(','); + } + + if (c.size() > 0) + out.cre(indent-1); + if (! plainTextParams) + out.append(')'); + + return out; } } http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/2a37f310/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingParser.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingParser.java b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingParser.java index d07d778..399c882 100644 --- a/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingParser.java +++ b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingParser.java @@ -16,14 +16,11 @@ import static org.apache.juneau.uon.UonParserContext.*; import static org.apache.juneau.internal.ArrayUtils.*; import static org.apache.juneau.internal.StringUtils.*; -import java.lang.reflect.*; import java.util.*; import org.apache.juneau.*; import org.apache.juneau.annotation.*; -import org.apache.juneau.http.*; import org.apache.juneau.parser.*; -import org.apache.juneau.transform.*; import org.apache.juneau.uon.*; /** @@ -51,7 +48,7 @@ import org.apache.juneau.uon.*; * <li>{@link BeanContext} * </ul> */ -@SuppressWarnings({ "rawtypes", "unchecked", "hiding" }) +@SuppressWarnings({ "unchecked", "hiding" }) @Consumes("application/x-www-form-urlencoded") public class UrlEncodingParser extends UonParser implements PartParser { @@ -67,272 +64,15 @@ public class UrlEncodingParser extends UonParser implements PartParser { * @param propertyStore The property store containing all the settings for this object. */ public UrlEncodingParser(PropertyStore propertyStore) { - super(propertyStore); + super(propertyStore.copy().append(UON_decodeChars, true)); this.ctx = createContext(UrlEncodingParserContext.class); } @Override /* CoreObject */ - public ObjectMap getOverrideProperties() { - return super.getOverrideProperties().append(UON_decodeChars, true); - } - - @Override /* CoreObject */ public UrlEncodingParserBuilder builder() { return new UrlEncodingParserBuilder(propertyStore); } - private <T> T parseAnything(UrlEncodingParserSession session, ClassMeta<T> eType, ParserReader r, Object outer) throws Exception { - - if (eType == null) - eType = (ClassMeta<T>)object(); - PojoSwap<T,Object> transform = (PojoSwap<T,Object>)eType.getPojoSwap(); - ClassMeta<?> sType = eType.getSerializedClassMeta(); - - int c = r.peekSkipWs(); - if (c == '?') - r.read(); - - Object o; - - if (sType.isObject()) { - ObjectMap m = new ObjectMap(session); - parseIntoMap(session, r, m, session.getClassMeta(Map.class, String.class, Object.class), outer); - if (m.containsKey("_value")) - o = m.get("_value"); - else - o = session.cast(m, null, eType); - } else if (sType.isMap()) { - Map m = (sType.canCreateNewInstance() ? (Map)sType.newInstance() : new ObjectMap(session)); - o = parseIntoMap(session, r, m, sType, m); - } else if (sType.canCreateNewBean(outer)) { - BeanMap m = session.newBeanMap(outer, sType.getInnerClass()); - m = parseIntoBeanMap(session, r, m); - o = m == null ? null : m.getBean(); - } else if (sType.isCollection() || sType.isArray() || sType.isArgs()) { - // ?1=foo&2=bar... - Collection c2 = ((sType.isArray() || sType.isArgs()) || ! sType.canCreateNewInstance(outer)) ? new ObjectList(session) : (Collection)sType.newInstance(); - Map<Integer,Object> m = new TreeMap<Integer,Object>(); - parseIntoMap(session, r, m, sType, c2); - c2.addAll(m.values()); - if (sType.isArray()) - o = toArray(c2, sType.getElementType().getInnerClass()); - else if (sType.isArgs()) - o = c2.toArray(new Object[c2.size()]); - else - o = c2; - } else { - // It could be a non-bean with _type attribute. - ObjectMap m = new ObjectMap(session); - parseIntoMap(session, r, m, session.getClassMeta(Map.class, String.class, Object.class), outer); - if (m.containsKey(session.getBeanTypePropertyName(eType))) - o = session.cast(m, null, eType); - else if (m.containsKey("_value")) { - o = session.convertToType(m.get("_value"), sType); - } else { - if (sType.getNotABeanReason() != null) - throw new ParseException(session, "Class ''{0}'' could not be instantiated as application/x-www-form-urlencoded. Reason: ''{1}''", sType, sType.getNotABeanReason()); - throw new ParseException(session, "Malformed application/x-www-form-urlencoded input for class ''{0}''.", sType); - } - } - - if (transform != null && o != null) - o = transform.unswap(session, o, eType); - - if (outer != null) - setParent(eType, o, outer); - - return (T)o; - } - - private <K,V> Map<K,V> parseIntoMap(UonParserSession session, ParserReader r, Map<K,V> m, ClassMeta<?> type, Object outer) throws Exception { - - ClassMeta<K> keyType = (ClassMeta<K>)(type.isArgs() || type.isCollectionOrArray() ? session.getClassMeta(Integer.class) : type.getKeyType()); - - int c = r.peekSkipWs(); - if (c == -1) - return m; - - 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 end. - boolean isInEscape = false; - - int state = S1; - int argIndex = 0; - K currAttr = null; - while (c != -1) { - c = r.read(); - if (! isInEscape) { - if (state == S1) { - if (c == -1) - return m; - r.unread(); - Object attr = parseAttr(session, r, true); - currAttr = attr == null ? null : convertAttrToType(session, m, session.trim(attr.toString()), keyType); - state = S2; - c = 0; // Avoid isInEscape if c was '\' - } else if (state == S2) { - if (c == '\u0002') - state = S3; - else if (c == -1 || c == '\u0001') { - m.put(currAttr, null); - if (c == -1) - return m; - state = S1; - } - } else if (state == S3) { - if (c == -1 || c == '\u0001') { - ClassMeta<V> valueType = (ClassMeta<V>)(type.isArgs() ? type.getArg(argIndex++) : type.isCollectionOrArray() ? type.getElementType() : type.getValueType()); - V value = convertAttrToType(session, m, "", valueType); - m.put(currAttr, value); - if (c == -1) - return m; - state = S1; - } else { - // For performance, we bypass parseAnything for string values. - ClassMeta<V> valueType = (ClassMeta<V>)(type.isArgs() ? type.getArg(argIndex++) : type.isCollectionOrArray() ? type.getElementType() : type.getValueType()); - V value = (V)(valueType.isString() ? super.parseString(session, r.unread(), true) : super.parseAnything(session, valueType, r.unread(), outer, true, null)); - - // If we already encountered this parameter, turn it into a list. - if (m.containsKey(currAttr) && valueType.isObject()) { - Object v2 = m.get(currAttr); - if (! (v2 instanceof ObjectList)) { - v2 = new ObjectList(v2).setBeanSession(session); - m.put(currAttr, (V)v2); - } - ((ObjectList)v2).add(value); - } else { - m.put(currAttr, value); - } - state = S4; - c = 0; // Avoid isInEscape if c was '\' - } - } else if (state == S4) { - if (c == '\u0001') - state = S1; - else if (c == -1) { - return m; - } - } - } - isInEscape = (c == '\\' && ! isInEscape); - } - if (state == S1) - throw new ParseException(session, "Could not find attribute name on object."); - if (state == S2) - throw new ParseException(session, "Could not find '=' following attribute name on object."); - if (state == S3) - throw new ParseException(session, "Dangling '=' found in object entry"); - if (state == S4) - throw new ParseException(session, "Could not find end of object."); - - return null; // Unreachable. - } - - private <T> BeanMap<T> parseIntoBeanMap(UrlEncodingParserSession session, ParserReader r, BeanMap<T> m) throws Exception { - - int c = r.peekSkipWs(); - if (c == -1) - return m; - - 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 = ""; - int currAttrLine = -1, currAttrCol = -1; - while (c != -1) { - c = r.read(); - if (! isInEscape) { - if (state == S1) { - if (c == -1) { - return m; - } - r.unread(); - currAttrLine= r.getLine(); - currAttrCol = r.getColumn(); - currAttr = parseAttrName(session, r, true); - if (currAttr == null) // Value was '%00' - return null; - state = S2; - } else if (state == S2) { - if (c == '\u0002') - state = S3; - else if (c == -1 || c == '\u0001') { - m.put(currAttr, null); - if (c == -1) - return m; - state = S1; - } - } else if (state == S3) { - if (c == -1 || c == '\u0001') { - if (! currAttr.equals(session.getBeanTypePropertyName(m.getClassMeta()))) { - BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr); - if (pMeta == null) { - session.onUnknownProperty(currAttr, m, currAttrLine, currAttrCol); - } else { - session.setCurrentProperty(pMeta); - // In cases of "&foo=", create an empty instance of the value if createable. - // Otherwise, leave it null. - ClassMeta<?> cm = pMeta.getClassMeta(); - if (cm.canCreateNewInstance()) - pMeta.set(m, currAttr, cm.newInstance()); - session.setCurrentProperty(null); - } - } - if (c == -1) - return m; - state = S1; - } else { - if (! currAttr.equals(session.getBeanTypePropertyName(m.getClassMeta()))) { - BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr); - if (pMeta == null) { - session.onUnknownProperty(currAttr, m, currAttrLine, currAttrCol); - parseAnything(session, object(), r.unread(), m.getBean(false), true, null); // Read content anyway to ignore it - } else { - session.setCurrentProperty(pMeta); - if (session.shouldUseExpandedParams(pMeta)) { - ClassMeta et = pMeta.getClassMeta().getElementType(); - Object value = parseAnything(session, et, r.unread(), m.getBean(false), true, pMeta); - setName(et, value, currAttr); - pMeta.add(m, currAttr, value); - } else { - ClassMeta<?> cm = pMeta.getClassMeta(); - Object value = parseAnything(session, cm, r.unread(), m.getBean(false), true, pMeta); - setName(cm, value, currAttr); - pMeta.set(m, currAttr, value); - } - session.setCurrentProperty(null); - } - } - state = S4; - } - } else if (state == S4) { - if (c == '\u0001') - state = S1; - else if (c == -1) { - return m; - } - } - } - isInEscape = (c == '\\' && ! isInEscape); - } - if (state == S1) - throw new ParseException(session, "Could not find attribute name on object."); - if (state == S2) - throw new ParseException(session, "Could not find '=' following attribute name on object."); - if (state == S3) - throw new ParseException(session, "Could not find value following '=' on object."); - if (state == S4) - throw new ParseException(session, "Could not find end of object."); - - return null; // Unreachable. - } - /** * Parse a URL query string into a simple map of key/value pairs. * @@ -348,7 +88,9 @@ public class UrlEncodingParser extends UonParser implements PartParser { if (isEmpty(qs)) return m; - UonReader r = new UonReader(qs, true); + // We're reading from a string, so we don't need to make sure close() is called on the pipe. + ParserPipe p = new ParserPipe(qs, false, false, null, null); + UonReader r = new UonReader(p, true); final int S1=1; // Looking for attrName start. final int S2=2; // Found attrName start, looking for = or & or end. @@ -434,15 +176,17 @@ public class UrlEncodingParser extends UonParser implements PartParser { if (x == 'n' && "null".equals(in)) return null; } - UonParserSession session = createParameterSession(in); + UonParserSession session = createParameterSession(); + ParserPipe pipe = session.createPipe(in); try { - UonReader r = session.getReader(); - return super.parseAnything(session, type, r, null, true, null); + UonReader r = session.getUonReader(pipe, false); + return session.parseAnything(type, r, null, true, null); } catch (ParseException e) { throw e; } catch (Exception e) { - throw new ParseException(session, e); + throw new ParseException(session.getLastLocation(), e); } finally { + pipe.close(); session.close(); } } @@ -453,25 +197,7 @@ public class UrlEncodingParser extends UonParser implements PartParser { //-------------------------------------------------------------------------------- @Override /* Parser */ - public UrlEncodingParserSession createSession(Object input, ObjectMap op, Method javaMethod, Object outer, Locale locale, TimeZone timeZone, MediaType mediaType) { - return new UrlEncodingParserSession(ctx, op, input, javaMethod, outer, locale, timeZone, mediaType); - } - - @Override /* Parser */ - protected <T> T doParse(ParserSession session, ClassMeta<T> type) throws Exception { - UrlEncodingParserSession s = (UrlEncodingParserSession)session; - UonReader r = s.getReader(); - T o = parseAnything(s, type, r, s.getOuter()); - return o; - } - - @Override /* ReaderParser */ - protected <K,V> Map<K,V> doParseIntoMap(ParserSession session, Map<K,V> m, Type keyType, Type valueType) throws Exception { - UrlEncodingParserSession s = (UrlEncodingParserSession)session; - UonReader r = s.getReader(); - if (r.peekSkipWs() == '?') - r.read(); - m = parseIntoMap(s, r, m, session.getClassMeta(Map.class, keyType, valueType), null); - return m; + public UrlEncodingParserSession createSession(ParserSessionArgs args) { + return new UrlEncodingParserSession(ctx, args); } } http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/2a37f310/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingParserSession.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingParserSession.java b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingParserSession.java index 5c1ff7e..f2a0c98 100644 --- a/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingParserSession.java +++ b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingParserSession.java @@ -12,20 +12,23 @@ // *************************************************************************************************************************** package org.apache.juneau.urlencoding; -import java.io.*; import java.lang.reflect.*; import java.util.*; import org.apache.juneau.*; -import org.apache.juneau.http.*; +import org.apache.juneau.internal.*; +import org.apache.juneau.parser.*; +import org.apache.juneau.transform.*; import org.apache.juneau.uon.*; /** * Session object that lives for the duration of a single use of {@link UrlEncodingParser}. * * <p> - * This class is NOT thread safe. It is meant to be discarded after one-time use. + * 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 UrlEncodingParserSession extends UonParserSession { private final boolean expandedParams; @@ -36,35 +39,16 @@ public class UrlEncodingParserSession extends UonParserSession { * @param ctx * The context creating this session object. * The context contains all the configuration settings for this object. - * @param input - * The input. - * Can be any of the following types: - * <ul> - * <li><jk>null</jk> - * <li>{@link Reader} - * <li>{@link CharSequence} - * <li>{@link InputStream} containing UTF-8 encoded text. - * <li>{@link File} containing system encoded text. - * </ul> - * @param op - * The override properties. - * These override any context properties defined in the context. - * @param javaMethod The java method that called this parser, usually the method in a REST servlet. - * @param outer The outer object for instantiating top-level non-static inner classes. - * @param locale - * The session locale. - * If <jk>null</jk>, then the locale defined on the context is used. - * @param timeZone - * The session timezone. - * If <jk>null</jk>, then the timezone defined on the context is used. - * @param mediaType The session media type (e.g. <js>"application/json"</js>). + * @param args + * Runtime session arguments. */ - public UrlEncodingParserSession(UrlEncodingParserContext ctx, ObjectMap op, Object input, Method javaMethod, Object outer, Locale locale, TimeZone timeZone, MediaType mediaType) { - super(ctx, op, input, javaMethod, outer, locale, timeZone, mediaType); - if (op == null || op.isEmpty()) { + protected UrlEncodingParserSession(UrlEncodingParserContext ctx, ParserSessionArgs args) { + super(ctx, args); + ObjectMap p = getProperties(); + if (p.isEmpty()) { expandedParams = ctx.expandedParams; } else { - expandedParams = op.getBoolean(UrlEncodingContext.URLENC_expandedParams, false); + expandedParams = p.getBoolean(UrlEncodingContext.URLENC_expandedParams, false); } } @@ -84,4 +68,272 @@ public class UrlEncodingParserSession extends UonParserSession { } return false; } + + @Override /* ParserSession */ + protected <T> T doParse(ParserPipe pipe, ClassMeta<T> type) throws Exception { + UonReader r = getUonReader(pipe, true); + T o = parseAnything(type, r, getOuter()); + return o; + } + + @Override /* ReaderParserSession */ + protected <K,V> Map<K,V> doParseIntoMap(ParserPipe pipe, Map<K,V> m, Type keyType, Type valueType) throws Exception { + UonReader r = getUonReader(pipe, true); + if (r.peekSkipWs() == '?') + r.read(); + m = parseIntoMap2(r, m, getClassMeta(Map.class, keyType, valueType), null); + return m; + } + + private <T> T parseAnything(ClassMeta<T> eType, UonReader r, Object outer) throws Exception { + + if (eType == null) + eType = (ClassMeta<T>)object(); + PojoSwap<T,Object> transform = (PojoSwap<T,Object>)eType.getPojoSwap(); + ClassMeta<?> sType = eType.getSerializedClassMeta(); + + int c = r.peekSkipWs(); + if (c == '?') + r.read(); + + Object o; + + if (sType.isObject()) { + ObjectMap m = new ObjectMap(this); + parseIntoMap2(r, m, getClassMeta(Map.class, String.class, Object.class), outer); + if (m.containsKey("_value")) + o = m.get("_value"); + else + o = cast(m, null, eType); + } else if (sType.isMap()) { + Map m = (sType.canCreateNewInstance() ? (Map)sType.newInstance() : new ObjectMap(this)); + o = parseIntoMap2(r, m, sType, m); + } else if (sType.canCreateNewBean(outer)) { + BeanMap m = newBeanMap(outer, sType.getInnerClass()); + m = parseIntoBeanMap(r, m); + o = m == null ? null : m.getBean(); + } else if (sType.isCollection() || sType.isArray() || sType.isArgs()) { + // ?1=foo&2=bar... + Collection c2 = ((sType.isArray() || sType.isArgs()) || ! sType.canCreateNewInstance(outer)) ? new ObjectList(this) : (Collection)sType.newInstance(); + Map<Integer,Object> m = new TreeMap<Integer,Object>(); + parseIntoMap2(r, m, sType, c2); + c2.addAll(m.values()); + if (sType.isArray()) + o = ArrayUtils.toArray(c2, sType.getElementType().getInnerClass()); + else if (sType.isArgs()) + o = c2.toArray(new Object[c2.size()]); + else + o = c2; + } else { + // It could be a non-bean with _type attribute. + ObjectMap m = new ObjectMap(this); + parseIntoMap2(r, m, getClassMeta(Map.class, String.class, Object.class), outer); + if (m.containsKey(getBeanTypePropertyName(eType))) + o = cast(m, null, eType); + else if (m.containsKey("_value")) { + o = convertToType(m.get("_value"), sType); + } else { + if (sType.getNotABeanReason() != null) + throw new ParseException(loc(r), "Class ''{0}'' could not be instantiated as application/x-www-form-urlencoded. Reason: ''{1}''", sType, sType.getNotABeanReason()); + throw new ParseException(loc(r), "Malformed application/x-www-form-urlencoded input for class ''{0}''.", sType); + } + } + + if (transform != null && o != null) + o = transform.unswap(this, o, eType); + + if (outer != null) + setParent(eType, o, outer); + + return (T)o; + } + + private <K,V> Map<K,V> parseIntoMap2(UonReader r, Map<K,V> m, ClassMeta<?> type, Object outer) throws Exception { + + ClassMeta<K> keyType = (ClassMeta<K>)(type.isArgs() || type.isCollectionOrArray() ? getClassMeta(Integer.class) : type.getKeyType()); + + int c = r.peekSkipWs(); + if (c == -1) + return m; + + 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 end. + boolean isInEscape = false; + + int state = S1; + int argIndex = 0; + K currAttr = null; + while (c != -1) { + c = r.read(); + if (! isInEscape) { + if (state == S1) { + if (c == -1) + return m; + r.unread(); + Object attr = parseAttr(r, true); + 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 == '\u0002') + state = S3; + else if (c == -1 || c == '\u0001') { + m.put(currAttr, null); + if (c == -1) + return m; + state = S1; + } + } else if (state == S3) { + if (c == -1 || c == '\u0001') { + ClassMeta<V> valueType = (ClassMeta<V>)(type.isArgs() ? type.getArg(argIndex++) : type.isCollectionOrArray() ? type.getElementType() : type.getValueType()); + V value = convertAttrToType(m, "", valueType); + m.put(currAttr, value); + if (c == -1) + return m; + state = S1; + } else { + // For performance, we bypass parseAnything for string values. + ClassMeta<V> valueType = (ClassMeta<V>)(type.isArgs() ? type.getArg(argIndex++) : type.isCollectionOrArray() ? type.getElementType() : type.getValueType()); + V value = (V)(valueType.isString() ? super.parseString(r.unread(), true) : super.parseAnything(valueType, r.unread(), outer, true, null)); + + // If we already encountered this parameter, turn it into a list. + if (m.containsKey(currAttr) && valueType.isObject()) { + Object v2 = m.get(currAttr); + if (! (v2 instanceof ObjectList)) { + v2 = new ObjectList(v2).setBeanSession(this); + m.put(currAttr, (V)v2); + } + ((ObjectList)v2).add(value); + } else { + m.put(currAttr, value); + } + state = S4; + c = 0; // Avoid isInEscape if c was '\' + } + } else if (state == S4) { + if (c == '\u0001') + state = S1; + else if (c == -1) { + return m; + } + } + } + isInEscape = (c == '\\' && ! isInEscape); + } + if (state == S1) + throw new ParseException(loc(r), "Could not find attribute name on object."); + if (state == S2) + throw new ParseException(loc(r), "Could not find '=' following attribute name on object."); + if (state == S3) + throw new ParseException(loc(r), "Dangling '=' found in object entry"); + if (state == S4) + throw new ParseException(loc(r), "Could not find end of object."); + + return null; // Unreachable. + } + + private <T> BeanMap<T> parseIntoBeanMap(UonReader r, BeanMap<T> m) throws Exception { + + int c = r.peekSkipWs(); + if (c == -1) + return m; + + 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 = ""; + int currAttrLine = -1, currAttrCol = -1; + while (c != -1) { + c = r.read(); + if (! isInEscape) { + if (state == S1) { + if (c == -1) { + return m; + } + r.unread(); + currAttrLine= r.getLine(); + currAttrCol = r.getColumn(); + currAttr = parseAttrName(r, true); + if (currAttr == null) // Value was '%00' + return null; + state = S2; + } else if (state == S2) { + if (c == '\u0002') + state = S3; + else if (c == -1 || c == '\u0001') { + m.put(currAttr, null); + if (c == -1) + return m; + state = S1; + } + } else if (state == S3) { + if (c == -1 || c == '\u0001') { + if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) { + BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr); + if (pMeta == null) { + onUnknownProperty(r.getPipe(), currAttr, m, currAttrLine, currAttrCol); + } else { + setCurrentProperty(pMeta); + // In cases of "&foo=", create an empty instance of the value if createable. + // Otherwise, leave it null. + ClassMeta<?> cm = pMeta.getClassMeta(); + if (cm.canCreateNewInstance()) + pMeta.set(m, currAttr, cm.newInstance()); + setCurrentProperty(null); + } + } + if (c == -1) + return m; + state = S1; + } else { + if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) { + BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr); + if (pMeta == null) { + onUnknownProperty(r.getPipe(), currAttr, m, currAttrLine, currAttrCol); + parseAnything(object(), r.unread(), m.getBean(false), true, null); // Read content anyway to ignore it + } else { + setCurrentProperty(pMeta); + if (shouldUseExpandedParams(pMeta)) { + ClassMeta et = pMeta.getClassMeta().getElementType(); + Object value = parseAnything(et, r.unread(), m.getBean(false), true, pMeta); + setName(et, value, currAttr); + pMeta.add(m, currAttr, value); + } else { + ClassMeta<?> cm = pMeta.getClassMeta(); + Object value = parseAnything(cm, r.unread(), m.getBean(false), true, pMeta); + setName(cm, value, currAttr); + pMeta.set(m, currAttr, value); + } + setCurrentProperty(null); + } + } + state = S4; + } + } else if (state == S4) { + if (c == '\u0001') + state = S1; + else if (c == -1) { + return m; + } + } + } + isInEscape = (c == '\\' && ! isInEscape); + } + if (state == S1) + throw new ParseException(loc(r), "Could not find attribute name on object."); + if (state == S2) + throw new ParseException(loc(r), "Could not find '=' following attribute name on object."); + if (state == S3) + throw new ParseException(loc(r), "Could not find value following '=' on object."); + if (state == S4) + throw new ParseException(loc(r), "Could not find end of object."); + + return null; // Unreachable. + } }
