Return-Path: X-Original-To: archive-asf-public-internal@cust-asf2.ponee.io Delivered-To: archive-asf-public-internal@cust-asf2.ponee.io Received: from cust-asf.ponee.io (cust-asf.ponee.io [163.172.22.183]) by cust-asf2.ponee.io (Postfix) with ESMTP id E271F200C42 for ; Fri, 10 Mar 2017 17:51:18 +0100 (CET) Received: by cust-asf.ponee.io (Postfix) id E0DA9160B67; Fri, 10 Mar 2017 16:51:18 +0000 (UTC) Delivered-To: archive-asf-public@cust-asf.ponee.io Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by cust-asf.ponee.io (Postfix) with SMTP id B8E1C160B82 for ; Fri, 10 Mar 2017 17:51:16 +0100 (CET) Received: (qmail 41040 invoked by uid 500); 10 Mar 2017 16:51:16 -0000 Mailing-List: contact commits-help@juneau.incubator.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@juneau.incubator.apache.org Delivered-To: mailing list commits@juneau.incubator.apache.org Received: (qmail 41031 invoked by uid 99); 10 Mar 2017 16:51:15 -0000 Received: from pnap-us-west-generic-nat.apache.org (HELO spamd1-us-west.apache.org) (209.188.14.142) by apache.org (qpsmtpd/0.29) with ESMTP; Fri, 10 Mar 2017 16:51:15 +0000 Received: from localhost (localhost [127.0.0.1]) by spamd1-us-west.apache.org (ASF Mail Server at spamd1-us-west.apache.org) with ESMTP id 60568C6B4D for ; Fri, 10 Mar 2017 16:51:15 +0000 (UTC) X-Virus-Scanned: Debian amavisd-new at spamd1-us-west.apache.org X-Spam-Flag: NO X-Spam-Score: -3.569 X-Spam-Level: X-Spam-Status: No, score=-3.569 tagged_above=-999 required=6.31 tests=[KAM_ASCII_DIVIDERS=0.8, RCVD_IN_DNSWL_HI=-5, RCVD_IN_MSPIKE_H3=-0.01, RCVD_IN_MSPIKE_WL=-0.01, RP_MATCHES_RCVD=-0.001, SPF_NEUTRAL=0.652] autolearn=disabled Received: from mx1-lw-eu.apache.org ([10.40.0.8]) by localhost (spamd1-us-west.apache.org [10.40.0.7]) (amavisd-new, port 10024) with ESMTP id vJ64dXZJjh96 for ; Fri, 10 Mar 2017 16:51:10 +0000 (UTC) Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by mx1-lw-eu.apache.org (ASF Mail Server at mx1-lw-eu.apache.org) with SMTP id 633005FDCE for ; Fri, 10 Mar 2017 16:50:58 +0000 (UTC) Received: (qmail 36121 invoked by uid 99); 10 Mar 2017 16:50:57 -0000 Received: from git1-us-west.apache.org (HELO git1-us-west.apache.org) (140.211.11.23) by apache.org (qpsmtpd/0.29) with ESMTP; Fri, 10 Mar 2017 16:50:57 +0000 Received: by git1-us-west.apache.org (ASF Mail Server at git1-us-west.apache.org, from userid 33) id 4E7BBF4B5A; Fri, 10 Mar 2017 16:50:57 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: jamesbognar@apache.org To: commits@juneau.incubator.apache.org Date: Fri, 10 Mar 2017 16:51:09 -0000 Message-Id: <9f5b07a5183546468317b2f85d5fcd5f@git.apache.org> In-Reply-To: <050f6b9ffdd24b72a491377772392c83@git.apache.org> References: <050f6b9ffdd24b72a491377772392c83@git.apache.org> X-Mailer: ASF-Git Admin Mailer Subject: [13/34] incubator-juneau git commit: Add builder classes for all serializers and parsers. archived-at: Fri, 10 Mar 2017 16:51:19 -0000 http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/95e832e1/juneau-core/src/main/java/org/apache/juneau/uon/UonParser.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/uon/UonParser.java b/juneau-core/src/main/java/org/apache/juneau/uon/UonParser.java new file mode 100644 index 0000000..ef4c95c --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/uon/UonParser.java @@ -0,0 +1,800 @@ +// *************************************************************************************************************************** +// * 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.uon.UonParserContext.*; + +import java.lang.reflect.*; +import java.util.*; + +import org.apache.juneau.*; +import org.apache.juneau.annotation.*; +import org.apache.juneau.internal.*; +import org.apache.juneau.parser.*; +import org.apache.juneau.transform.*; + +/** + * Parses UON (a notation for URL-encoded query parameter values) text into POJO models. + * + *
Media types:
+ *

+ * Handles Content-Type types: text/uon + * + *

Description:
+ *

+ * This parser uses a state machine, which makes it very fast and efficient. + * + *

Configurable properties:
+ *

+ * This class has the following properties associated with it: + *

    + *
  • {@link UonParserContext} + *
  • {@link ParserContext} + *
  • {@link BeanContext} + *
+ */ +@SuppressWarnings({ "rawtypes", "unchecked" }) +@Consumes("text/uon") +public class UonParser extends ReaderParser { + + /** Reusable instance of {@link UonParser}, all default settings. */ + public static final UonParser DEFAULT = new UonParser(PropertyStore.create()); + + /** Reusable instance of {@link UonParser} with decodeChars set to true. */ + public static final UonParser DEFAULT_DECODING = new UonParser.Decoding(PropertyStore.create()); + + // Characters that need to be preceeded 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. + + + /** Default parser, decoding. */ + public static class Decoding extends UonParser { + + /** + * Constructor. + * @param propertyStore The property store containing all the settings for this object. + */ + public Decoding(PropertyStore propertyStore) { + super(propertyStore); + } + + @Override /* CoreObject */ + protected ObjectMap getOverrideProperties() { + return super.getOverrideProperties().append(UON_decodeChars, true); + } + } + + + private final UonParserContext ctx; + + /** + * Constructor. + * @param propertyStore The property store containing all the settings for this object. + */ + public UonParser(PropertyStore propertyStore) { + super(propertyStore); + this.ctx = createContext(UonParserContext.class); + } + + @Override /* CoreObject */ + public UonParserBuilder builder() { + return new UonParserBuilder(propertyStore); + } + + /** + * Workhorse method. + * + * @param session The parser context for this parse. + * @param eType The class type being parsed, or null if unknown. + * @param r The reader being parsed. + * @param outer The outer object (for constructing nested inner classes). + * @param isUrlParamValue If true, 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 + */ + protected T parseAnything(UonParserSession session, ClassMeta eType, ParserReader r, Object outer, boolean isUrlParamValue, BeanPropertyMeta pMeta) throws Exception { + + if (eType == null) + eType = (ClassMeta)object(); + PojoSwap transform = (PojoSwap)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.isObject()) { + if (c == '(') { + ObjectMap m = new ObjectMap(session); + parseIntoMap(session, r, m, string(), object(), pMeta); + o = session.cast(m, pMeta, eType); + } else if (c == '@') { + Collection l = new ObjectList(session); + o = parseIntoCollection(session, r, l, sType.getElementType(), isUrlParamValue, pMeta); + } else { + String s = parseString(session, r, isUrlParamValue); + if (c != '\'') { + if ("true".equals(s) || "false".equals(s)) + o = Boolean.valueOf(s); + else if (StringUtils.isNumeric(s)) + o = StringUtils.parseNumber(s, Number.class); + else + o = s; + } else { + o = s; + } + } + } else if (sType.isBoolean()) { + o = parseBoolean(session, r); + } else if (sType.isCharSequence()) { + o = parseString(session, r, isUrlParamValue); + } else if (sType.isChar()) { + String s = parseString(session, r, isUrlParamValue); + o = s == null ? null : s.charAt(0); + } else if (sType.isNumber()) { + o = parseNumber(session, r, (Class)sType.getInnerClass()); + } else if (sType.isMap()) { + Map m = (sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : new ObjectMap(session)); + o = parseIntoMap(session, r, m, sType.getKeyType(), sType.getValueType(), pMeta); + } else if (sType.isCollection()) { + if (c == '(') { + ObjectMap m = new ObjectMap(session); + parseIntoMap(session, 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(session.getBeanTypePropertyName())) + o = session.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(session)); + l.add(m.cast(sType.getElementType())); + o = l; + } + } else { + Collection l = (sType.canCreateNewInstance(outer) ? (Collection)sType.newInstance(outer) : new ObjectList(session)); + o = parseIntoCollection(session, r, l, sType.getElementType(), isUrlParamValue, pMeta); + } + } 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.canCreateNewInstanceFromString(outer)) { + String s = parseString(session, r, isUrlParamValue); + if (s != null) + o = sType.newInstanceFromString(outer, s); + } else if (sType.canCreateNewInstanceFromNumber(outer)) { + o = sType.newInstanceFromNumber(session, outer, parseNumber(session, r, sType.getNewInstanceFromNumberClass())); + } else if (sType.isArray()) { + if (c == '(') { + ObjectMap m = new ObjectMap(session); + parseIntoMap(session, 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(session.getBeanTypePropertyName())) + o = session.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 = session.toArray(sType, l); + } + } else { + ArrayList l = (ArrayList)parseIntoCollection(session, r, new ArrayList(), sType.getElementType(), isUrlParamValue, pMeta); + o = session.toArray(sType, l); + } + } else if (c == '(') { + // It could be a non-bean with _type attribute. + ObjectMap m = new ObjectMap(session); + parseIntoMap(session, r, m, string(), object(), pMeta); + if (m.containsKey(session.getBeanTypePropertyName())) + o = session.cast(m, pMeta, eType); + else + throw new ParseException(session, "Class ''{0}'' could not be instantiated. Reason: ''{1}''", sType.getInnerClass().getName(), sType.getNotABeanReason()); + } else { + throw new ParseException(session, "Class ''{0}'' could not be instantiated. Reason: ''{1}''", sType.getInnerClass().getName(), sType.getNotABeanReason()); + } + + if (transform != null && o != null) + o = transform.unswap(session, o, eType); + + if (outer != null) + setParent(eType, o, outer); + + return (T)o; + } + + private Map parseIntoMap(UonParserSession session, ParserReader r, Map m, ClassMeta keyType, ClassMeta valueType, BeanPropertyMeta pMeta) throws Exception { + + if (keyType == null) + keyType = (ClassMeta)string(); + + int c = r.read(); + if (c == -1 || c == AMP) + return null; + if (c == 'n') + return (Map)parseNull(session, r); + if (c != '(') + throw new ParseException(session, "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(session, r, session.isDecodeChars()); + 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 == 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(session, m, "", valueType); + m.put(currAttr, value); + if (c == -1 || c == ')' || c == AMP) + return m; + state = S1; + } else { + V value = parseAnything(session, 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(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 ')' marking end of object."); + + return null; // Unreachable. + } + + private Collection parseIntoCollection(UonParserSession session, ParserReader r, Collection l, ClassMeta elementType, boolean isUrlParamValue, BeanPropertyMeta pMeta) throws Exception { + + int c = r.readSkipWs(); + if (c == -1 || c == AMP) + return null; + if (c == 'n') + return (Collection)parseNull(session, r); + + // 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(session, "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(parseAnything(session, elementType, r.unread(), l, false, pMeta)); + r.read(); + } + return l; + } else if (Character.isWhitespace(c)) { + skipSpace(r); + } else { + l.add(parseAnything(session, elementType, 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(session, "Could not find start of entry in array."); + if (state == S3) + throw new ParseException(session, "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(parseAnything(session, elementType, 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 BeanMap parseIntoBeanMap(UonParserSession session, ParserReader r, BeanMap m) throws Exception { + + int c = r.readSkipWs(); + if (c == -1 || c == AMP) + return null; + if (c == 'n') + return (BeanMap)parseNull(session, r); + if (c != '(') + throw new ParseException(session, "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(session, r, session.isDecodeChars()); + 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(session.getBeanTypePropertyName())) { + BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr); + if (pMeta == null) { + onUnknownProperty(session, currAttr, m, currAttrLine, currAttrCol); + } else { + Object value = session.convertToType("", pMeta.getClassMeta()); + pMeta.set(m, value); + } + } + if (c == -1 || c == ')' || c == AMP) + return m; + state = S1; + } else { + if (! currAttr.equals(session.getBeanTypePropertyName())) { + BeanPropertyMeta pMeta = m.getPropertyMeta(currAttr); + if (pMeta == null) { + onUnknownProperty(session, currAttr, m, currAttrLine, currAttrCol); + parseAnything(session, object(), r.unread(), m.getBean(false), false, null); // Read content anyway to ignore it + } else { + session.setCurrentProperty(pMeta); + ClassMeta cm = pMeta.getClassMeta(); + Object value = parseAnything(session, cm, r.unread(), m.getBean(false), false, pMeta); + setName(cm, value, currAttr); + pMeta.set(m, value); + session.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(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 ')' marking end of object."); + + return null; // Unreachable. + } + + private Object parseNull(UonParserSession session, ParserReader r) throws Exception { + String s = parseString(session, r, false); + if ("ull".equals(s)) + return null; + throw new ParseException(session, "Unexpected character sequence: ''{0}''", s); + } + + /** + * Convenience method for parsing an attribute from the specified parser. + * + * @param session + * @param r + * @param encoded + * @return The parsed object + * @throws Exception + */ + protected final Object parseAttr(UonParserSession session, ParserReader r, boolean encoded) throws Exception { + Object attr; + attr = parseAttrName(session, r, encoded); + return attr; + } + + /** + * Parses an attribute name from the specified reader. + * + * @param session + * @param r + * @param encoded + * @return The parsed attribute name. + * @throws Exception + */ + protected final String parseAttrName(UonParserSession session, ParserReader 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(session, 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 : session.trim(s)); + } + } + isInEscape = isInEscape(c, r, isInEscape); + } + } + + // We should never get here. + throw new ParseException(session, "Unexpected condition."); + } + + + /** + * Returns true if the next character in the stream is preceeded by an escape '~' character. + * @param c The current character. + * @param r The reader. + * @param prevIsInEscape What the flag was last time. + */ + 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 session + * @param r + * @param isUrlParamValue + * @return The parsed string. + * @throws Exception + */ + protected final String parseString(UonParserSession session, ParserReader 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(session, 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 : session.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. + */ + static String parsePString(UonParserSession session, ParserReader 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 session.trim(r.getMarked(0, -1)); + } + if (c == EQ) + r.replace('='); + isInEscape = isInEscape(c, r, isInEscape); + } + throw new ParseException(session, "Unmatched parenthesis"); + } + + private Boolean parseBoolean(UonParserSession session, ParserReader r) throws Exception { + String s = parseString(session, 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(session, "Unrecognized syntax for boolean. ''{0}''.", s); + } + + private Number parseNumber(UonParserSession session, ParserReader r, Class c) throws Exception { + String s = parseString(session, 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(UonParserSession session, ParserReader r) throws Exception { + while (true) { + int c = r.read(); + if (c == -1) + return; + if (! Character.isWhitespace(c)) + throw new ParseException(session, "Remainder after parse: ''{0}''.", (char)c); + } + } + + private Object[] parseArgs(UonParserSession session, ParserReader r, ClassMeta[] argTypes) throws Exception { + + final int S1=1; // Looking for start of entry + final int S2=2; // Looking for , or ) + + Object[] o = new Object[argTypes.length]; + int i = 0; + + int c = r.readSkipWs(); + if (c == -1 || c == AMP) + return null; + if (c != '@') + throw new ParseException(session, "Expected '@' at beginning of args array."); + c = r.read(); + + int state = S1; + while (c != -1 && c != AMP) { + c = r.read(); + if (state == S1) { + if (c == ')') + return o; + o[i] = parseAnything(session, argTypes[i], r.unread(), session.getOuter(), false, null); + i++; + state = S2; + } else if (state == S2) { + if (c == ',') { + state = S1; + } else if (c == ')') { + return o; + } + } + } + + throw new ParseException(session, "Did not find ')' at the end of args array."); + } + + 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; + } + } + } + + /** + * Create a UON parser session for parsing parameter values. + * + * @param input + * @return A new parser session. + */ + protected final UonParserSession createParameterSession(Object input) { + return new UonParserSession(ctx, input); + } + + + //-------------------------------------------------------------------------------- + // Entry point methods + //-------------------------------------------------------------------------------- + + @Override /* Parser */ + public UonParserSession createSession(Object input, ObjectMap op, Method javaMethod, Object outer, Locale locale, TimeZone timeZone, MediaType mediaType) { + return new UonParserSession(ctx, op, input, javaMethod, outer, locale, timeZone, mediaType); + } + + @Override /* Parser */ + protected T doParse(ParserSession session, ClassMeta type) throws Exception { + UonParserSession s = (UonParserSession)session; + UonReader r = s.getReader(); + T o = parseAnything(s, type, r, s.getOuter(), true, null); + validateEnd(s, r); + return o; + } + + @Override /* ReaderParser */ + protected Map doParseIntoMap(ParserSession session, Map m, Type keyType, Type valueType) throws Exception { + UonParserSession s = (UonParserSession)session; + UonReader r = s.getReader(); + m = parseIntoMap(s, r, m, (ClassMeta)session.getClassMeta(keyType), (ClassMeta)session.getClassMeta(valueType), null); + validateEnd(s, r); + return m; + } + + @Override /* ReaderParser */ + protected Collection doParseIntoCollection(ParserSession session, Collection c, Type elementType) throws Exception { + UonParserSession s = (UonParserSession)session; + UonReader r = s.getReader(); + c = parseIntoCollection(s, r, c, (ClassMeta)session.getClassMeta(elementType), false, null); + validateEnd(s, r); + return c; + } + + @Override /* ReaderParser */ + protected Object[] doParseArgs(ParserSession session, ClassMeta[] argTypes) throws Exception { + UonParserSession s = (UonParserSession)session; + UonReader r = s.getReader(); + Object[] a = parseArgs(s, r, argTypes); + return a; + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/95e832e1/juneau-core/src/main/java/org/apache/juneau/uon/UonParserBuilder.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/uon/UonParserBuilder.java b/juneau-core/src/main/java/org/apache/juneau/uon/UonParserBuilder.java new file mode 100644 index 0000000..694a97e --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/uon/UonParserBuilder.java @@ -0,0 +1,495 @@ +// *************************************************************************************************************************** +// * 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.uon.UonParserContext.*; + +import java.util.*; + +import org.apache.juneau.*; +import org.apache.juneau.parser.*; +import org.apache.juneau.urlencoding.*; + +/** + * Builder class for building instances of UON parsers. + */ +public class UonParserBuilder extends ParserBuilder { + + /** + * Constructor, default settings. + */ + public UonParserBuilder() { + super(); + } + + /** + * Constructor. + * @param propertyStore The initial configuration settings for this builder. + */ + public UonParserBuilder(PropertyStore propertyStore) { + super(propertyStore); + } + + @Override /* CoreObjectBuilder */ + public UonParser build() { + return new UonParser(propertyStore); + } + + + //-------------------------------------------------------------------------------- + // Properties + //-------------------------------------------------------------------------------- + + /** + * Configuration property: Decode "%xx" sequences. + *

+ *

    + *
  • Name: "UonParser.decodeChars" + *
  • Data type: Boolean + *
  • Default: false for {@link UonParser}, true for {@link UrlEncodingParser} + *
  • Session-overridable: true + *
+ *

+ * Specify true if URI encoded characters should be decoded, false + * if they've already been decoded before being passed to this parser. + *

+ *

Notes:
+ *
    + *
  • This is equivalent to calling property(UON_decodeChars, value). + *
+ * + * @param value The new value for this property. + * @return This object (for method chaining). + * @see UonParserContext#UON_decodeChars + */ + public UonParserBuilder decodeChars(boolean value) { + return property(UON_decodeChars, value); + } + + /** + * Shortcut for calling decodeChars(true). + * + * @return This object (for method chaining). + */ + public UonParserBuilder decoding() { + return decodeChars(true); + } + + @Override /* ParserBuilder */ + public UonParserBuilder trimStrings(boolean value) { + super.trimStrings(value); + return this; + } + + @Override /* ParserBuilder */ + public UonParserBuilder strict(boolean value) { + super.strict(value); + return this; + } + + @Override /* ParserBuilder */ + public UonParserBuilder strict() { + super.strict(); + return this; + } + + @Override /* ParserBuilder */ + public UonParserBuilder inputStreamCharset(String value) { + super.inputStreamCharset(value); + return this; + } + + @Override /* ParserBuilder */ + public UonParserBuilder fileCharset(String value) { + super.fileCharset(value); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder beansRequireDefaultConstructor(boolean value) { + super.beansRequireDefaultConstructor(value); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder beansRequireSerializable(boolean value) { + super.beansRequireSerializable(value); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder beansRequireSettersForGetters(boolean value) { + super.beansRequireSettersForGetters(value); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder beansRequireSomeProperties(boolean value) { + super.beansRequireSomeProperties(value); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder beanMapPutReturnsOldValue(boolean value) { + super.beanMapPutReturnsOldValue(value); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder beanConstructorVisibility(Visibility value) { + super.beanConstructorVisibility(value); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder beanClassVisibility(Visibility value) { + super.beanClassVisibility(value); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder beanFieldVisibility(Visibility value) { + super.beanFieldVisibility(value); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder methodVisibility(Visibility value) { + super.methodVisibility(value); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder useJavaBeanIntrospector(boolean value) { + super.useJavaBeanIntrospector(value); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder useInterfaceProxies(boolean value) { + super.useInterfaceProxies(value); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder ignoreUnknownBeanProperties(boolean value) { + super.ignoreUnknownBeanProperties(value); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder ignoreUnknownNullBeanProperties(boolean value) { + super.ignoreUnknownNullBeanProperties(value); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder ignorePropertiesWithoutSetters(boolean value) { + super.ignorePropertiesWithoutSetters(value); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder ignoreInvocationExceptionsOnGetters(boolean value) { + super.ignoreInvocationExceptionsOnGetters(value); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder ignoreInvocationExceptionsOnSetters(boolean value) { + super.ignoreInvocationExceptionsOnSetters(value); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder sortProperties(boolean value) { + super.sortProperties(value); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder notBeanPackages(String...values) { + super.notBeanPackages(values); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder notBeanPackages(Collection values) { + super.notBeanPackages(values); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder setNotBeanPackages(String...values) { + super.setNotBeanPackages(values); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder setNotBeanPackages(Collection values) { + super.setNotBeanPackages(values); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder removeNotBeanPackages(String...values) { + super.removeNotBeanPackages(values); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder removeNotBeanPackages(Collection values) { + super.removeNotBeanPackages(values); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder notBeanClasses(Class...values) { + super.notBeanClasses(values); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder notBeanClasses(Collection> values) { + super.notBeanClasses(values); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder setNotBeanClasses(Class...values) { + super.setNotBeanClasses(values); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder setNotBeanClasses(Collection> values) { + super.setNotBeanClasses(values); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder removeNotBeanClasses(Class...values) { + super.removeNotBeanClasses(values); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder removeNotBeanClasses(Collection> values) { + super.removeNotBeanClasses(values); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder beanFilters(Class...values) { + super.beanFilters(values); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder beanFilters(Collection> values) { + super.beanFilters(values); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder setBeanFilters(Class...values) { + super.setBeanFilters(values); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder setBeanFilters(Collection> values) { + super.setBeanFilters(values); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder removeBeanFilters(Class...values) { + super.removeBeanFilters(values); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder removeBeanFilters(Collection> values) { + super.removeBeanFilters(values); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder pojoSwaps(Class...values) { + super.pojoSwaps(values); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder pojoSwaps(Collection> values) { + super.pojoSwaps(values); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder setPojoSwaps(Class...values) { + super.setPojoSwaps(values); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder setPojoSwaps(Collection> values) { + super.setPojoSwaps(values); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder removePojoSwaps(Class...values) { + super.removePojoSwaps(values); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder removePojoSwaps(Collection> values) { + super.removePojoSwaps(values); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder implClasses(Map,Class> values) { + super.implClasses(values); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder implClass(Class interfaceClass, Class implClass) { + super.implClass(interfaceClass, implClass); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder beanDictionary(Class...values) { + super.beanDictionary(values); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder beanDictionary(Collection> values) { + super.beanDictionary(values); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder setBeanDictionary(Class...values) { + super.setBeanDictionary(values); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder setBeanDictionary(Collection> values) { + super.setBeanDictionary(values); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder removeFromBeanDictionary(Class...values) { + super.removeFromBeanDictionary(values); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder removeFromBeanDictionary(Collection> values) { + super.removeFromBeanDictionary(values); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder beanTypePropertyName(String value) { + super.beanTypePropertyName(value); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder defaultParser(Class value) { + super.defaultParser(value); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder locale(Locale value) { + super.locale(value); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder timeZone(TimeZone value) { + super.timeZone(value); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder mediaType(MediaType value) { + super.mediaType(value); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder debug(boolean value) { + super.debug(value); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder property(String name, Object value) { + super.property(name, value); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder properties(Map properties) { + super.properties(properties); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder addToProperty(String name, Object value) { + super.addToProperty(name, value); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder putToProperty(String name, Object key, Object value) { + super.putToProperty(name, key, value); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder putToProperty(String name, Object value) { + super.putToProperty(name, value); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder removeFromProperty(String name, Object value) { + super.removeFromProperty(name, value); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder classLoader(ClassLoader classLoader) { + super.classLoader(classLoader); + return this; + } + + @Override /* CoreObjectBuilder */ + public UonParserBuilder apply(PropertyStore copyFrom) { + super.apply(copyFrom); + return this; + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/95e832e1/juneau-core/src/main/java/org/apache/juneau/uon/UonParserContext.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/uon/UonParserContext.java b/juneau-core/src/main/java/org/apache/juneau/uon/UonParserContext.java new file mode 100644 index 0000000..7217f9d --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/uon/UonParserContext.java @@ -0,0 +1,74 @@ +// *************************************************************************************************************************** +// * 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 org.apache.juneau.*; +import org.apache.juneau.parser.*; +import org.apache.juneau.urlencoding.*; + +/** + * Configurable properties on the {@link UonParser} class. + *

+ * Context properties are set by calling {@link PropertyStore#setProperty(String, Object)} on the property store + * passed into the constructor. + *

+ * See {@link PropertyStore} for more information about context properties. + * + *

Inherited configurable properties:
+ *
    + *
  • BeanContext - Properties associated with handling beans on serializers and parsers. + *
      + *
    • ParserContext - Configurable properties common to all parsers. + *
    + *
+ */ +public class UonParserContext extends ParserContext { + + /** + * Configuration property: Decode "%xx" sequences. + *

+ *

    + *
  • Name: "UonParser.decodeChars" + *
  • Data type: Boolean + *
  • Default: false for {@link UonParser}, true for {@link UrlEncodingParser} + *
  • Session-overridable: true + *
+ *

+ * Specify true if URI encoded characters should be decoded, false + * if they've already been decoded before being passed to this parser. + */ + public static final String UON_decodeChars = "UonParser.decodeChars"; + + final boolean + decodeChars; + + /** + * Constructor. + *

+ * Typically only called from {@link PropertyStore#getContext(Class)}. + * + * @param ps The property store that created this context. + */ + public UonParserContext(PropertyStore ps) { + super(ps); + this.decodeChars = ps.getProperty(UON_decodeChars, boolean.class, false); + } + + @Override /* Context */ + public ObjectMap asMap() { + return super.asMap() + .append("UonParserContext", new ObjectMap() + .append("decodeChars", decodeChars) + ); + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/95e832e1/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 new file mode 100644 index 0000000..bf2334b --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/uon/UonParserSession.java @@ -0,0 +1,118 @@ +// *************************************************************************************************************************** +// * 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.uon.UonParserContext.*; + +import java.io.*; +import java.lang.reflect.*; +import java.util.*; + +import org.apache.juneau.*; +import org.apache.juneau.parser.*; + +/** + * Session object that lives for the duration of a single use of {@link UonParser}. + *

+ * This class is NOT thread safe. It is meant to be discarded after one-time use. + */ +public class UonParserSession extends ParserSession { + + private final boolean decodeChars; + private UonReader reader; + + /** + * Create a new session using properties specified in the context. + * + * @param ctx The context creating this session object. + * he context contains all the configuration settings for this object. + * @param input The input. Can be any of the following types: + *

    + *
  • null + *
  • {@link Reader} + *
  • {@link CharSequence} + *
  • {@link InputStream} containing UTF-8 encoded text. + *
  • {@link File} containing system encoded text. + *
+ * @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 null, then the locale defined on the context is used. + * @param timeZone The session timezone. + * If null, then the timezone defined on the context is used. + * @param mediaType The session media type (e.g. "application/json"). + */ + 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()) { + decodeChars = ctx.decodeChars; + } else { + decodeChars = op.getBoolean(UON_decodeChars, ctx.decodeChars); + } + } + + /** + * Create a specialized parser session for parsing URL parameters. + *

+ * The main difference is that characters are never decoded, and the {@link UonParserContext#UON_decodeChars} property is always ignored. + * + * @param ctx The context to copy setting from. + * @param input The input. Can be any of the following types: + *

    + *
  • null + *
  • {@link Reader} + *
  • {@link CharSequence} (e.g. {@link String}) + *
  • {@link InputStream} - Read as UTF-8 encoded character stream. + *
  • {@link File} - Read as system-default encoded stream. + *
+ */ + public UonParserSession(UonParserContext ctx, Object input) { + super(ctx, null, input, null, null, null, null, null); + decodeChars = false; + } + + /** + * Returns the {@link UonParserContext#UON_decodeChars} setting value for this session. + * + * @return The {@link UonParserContext#UON_decodeChars} setting value for this session. + */ + public final boolean isDecodeChars() { + return decodeChars; + } + + @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); + else + reader = new UonReader(super.getReader(), decodeChars); + } + return reader; + } + + @Override /* ParserSession */ + public Map getLastLocation() { + Map m = super.getLastLocation(); + if (reader != null) { + m.put("line", reader.getLine()); + m.put("column", reader.getColumn()); + } + return m; + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/95e832e1/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 new file mode 100644 index 0000000..eba2df4 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/uon/UonReader.java @@ -0,0 +1,195 @@ +// *************************************************************************************************************************** +// * 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 java.io.*; + +import org.apache.juneau.parser.*; + +/** + * Same functionality as {@link ParserReader} except automatically decoded %xx escape sequences. + *

+ * Escape sequences are assumed to be encoded UTF-8. Extended Unicode (>\u10000) is supported. + *

+ * If decoding is enabled, the following character replacements occur so that boundaries are not lost: + *

    + *
  • '&' -> '\u0001' + *
  • '=' -> '\u0002' + *
+ */ +public final class UonReader extends ParserReader { + + private final boolean decodeChars; + private final char[] buff; + private int iCurrent, iEnd; + + /** + * Constructor for input from a {@link CharSequence}. + * + * @param in The character sequence being read from. + * @param decodeChars If true, decode %xx 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}). + * + * @param r The Reader being wrapped. + * @param decodeChars If true, decode %xx escape sequences. + */ + public UonReader(Reader r, boolean decodeChars) { + super(r); + this.decodeChars = decodeChars; + this.buff = new char[1024]; + } + + @Override /* Reader */ + public final int read(char[] cbuf, int off, int len) throws IOException { + + if (! decodeChars) + return super.read(cbuf, off, len); + + // Copy any remainder to the beginning of the buffer. + int remainder = iEnd - iCurrent; + if (remainder > 0) + System.arraycopy(buff, iCurrent, buff, 0, remainder); + iCurrent = 0; + + int expected = buff.length - remainder; + + int x = super.read(buff, remainder, expected); + if (x == -1 && remainder == 0) + return -1; + + iEnd = remainder + (x == -1 ? 0 : x); + + int i = 0; + while (i < len) { + if (iCurrent >= iEnd) + return i; + char c = buff[iCurrent++]; + if (c == '+') { + cbuf[off + i++] = ' '; + } else if (c == '&') { + cbuf[off + i++] = '\u0001'; + } else if (c == '=') { + cbuf[off + i++] = '\u0002'; + } else if (c != '%') { + cbuf[off + i++] = c; + } else { + int iMark = iCurrent-1; // Keep track of current position. + + // Stop if there aren't at least two more characters following '%' in the buffer, + // or there aren't at least two more positions open in cbuf to handle double-char chars. + if (iMark+2 >= iEnd || i+2 > len) { + iCurrent--; + return i; + } + + int b0 = readEncodedByte(); + int cx; + + // 0xxxxxxx + if (b0 < 128) { + cx = b0; + + // 10xxxxxx + } else if (b0 < 192) { + throw new IOException("Invalid hex value for first escape pattern in UTF-8 sequence: " + b0); + + // 110xxxxx 10xxxxxx + // 11000000(192) - 11011111(223) + } else if (b0 < 224) { + cx = readUTF8(b0-192, 1); + if (cx == -1) { + iCurrent = iMark; + return i; + } + + // 1110xxxx 10xxxxxx 10xxxxxx + // 11100000(224) - 11101111(239) + } else if (b0 < 240) { + cx = readUTF8(b0-224, 2); + if (cx == -1) { + iCurrent = iMark; + return i; + } + + // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + // 11110000(240) - 11110111(247) + } else if (b0 < 248) { + cx = readUTF8(b0-240, 3); + if (cx == -1) { + iCurrent = iMark; + return i; + } + + } else + throw new IOException("Invalid hex value for first escape pattern in UTF-8 sequence: " + b0); + + if (cx < 0x10000) + cbuf[off + i++] = (char)cx; + else { + cx -= 0x10000; + cbuf[off + i++] = (char)(0xd800 + (cx >> 10)); + cbuf[off + i++] = (char)(0xdc00 + (cx & 0x3ff)); + } + } + } + return i; + } + + private final int readUTF8(int n, final int numBytes) throws IOException { + if (iCurrent + numBytes*3 > iEnd) + return -1; + for (int i = 0; i < numBytes; i++) { + n <<= 6; + n += readHex()-128; + } + return n; + } + + private final 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 { + if (iEnd <= iCurrent + 1) + throw new IOException("Incomplete trailing escape pattern"); + int h = buff[iCurrent++]; + int l = buff[iCurrent++]; + h = fromHexChar(h); + l = fromHexChar(l); + return (h << 4) + l; + } + + private final int fromHexChar(int c) throws IOException { + if (c >= '0' && c <= '9') + return c - '0'; + if (c >= 'a' && c <= 'f') + return 10 + c - 'a'; + if (c >= 'A' && c <= 'F') + return 10 + c - 'A'; + throw new IOException("Invalid hex character '"+c+"' found in escape pattern."); + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/95e832e1/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 new file mode 100644 index 0000000..6ecb85d --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/uon/UonSerializer.java @@ -0,0 +1,386 @@ +// *************************************************************************************************************************** +// * 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.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.serializer.*; +import org.apache.juneau.transform.*; + +/** + * Serializes POJO models to UON (a notation for URL-encoded query parameter values). + * + *
Media types:
+ *

+ * Handles Accept types: text/uon + *

+ * Produces Content-Type types: text/uon + * + *

Description:
+ *

+ * This serializer provides several serialization options. Typically, one of the predefined DEFAULT serializers will be sufficient. + * However, custom serializers can be constructed to fine-tune behavior. + * + *

Configurable properties:
+ *

+ * This class has the following properties associated with it: + *

    + *
  • {@link UonSerializerContext} + *
  • {@link BeanContext} + *
+ *

+ * The following shows a sample object defined in Javascript: + *

+ *

+ * { + * id: 1, + * name: 'John Smith', + * uri: 'http://sample/addressBook/person/1', + * addressBookUri: 'http://sample/addressBook', + * birthDate: '1946-08-12T00:00:00Z', + * otherIds: null, + * addresses: [ + * { + * uri: 'http://sample/addressBook/address/1', + * personUri: 'http://sample/addressBook/person/1', + * id: 1, + * street: '100 Main Street', + * city: 'Anywhereville', + * state: 'NY', + * zip: 12345, + * isCurrent: true, + * } + * ] + * } + *

+ *

+ * Using the "strict" syntax defined in this document, the equivalent + * UON notation would be as follows: + *

+ *

+ * ( + * id=1, + * name='John+Smith', + * uri=http://sample/addressBook/person/1, + * addressBookUri=http://sample/addressBook, + * birthDate=1946-08-12T00:00:00Z, + * otherIds=null, + * addresses=@( + * ( + * uri=http://sample/addressBook/address/1, + * personUri=http://sample/addressBook/person/1, + * id=1, + * street='100+Main+Street', + * city=Anywhereville, + * state=NY, + * zip=12345, + * isCurrent=true + * ) + * ) + * ) + *

+ * + *
Example:
+ *

+ * // Serialize a Map + * Map m = new ObjectMap("{a:'b',c:1,d:false,e:['f',1,false],g:{h:'i'}}"); + * + * // Serialize to value equivalent to JSON. + * // Produces "(a=b,c=1,d=false,e=@(f,1,false),g=(h=i))" + * String s = UonSerializer.DEFAULT.serialize(s); + * + * // Serialize a bean + * public class Person { + * public Person(String s); + * public String getName(); + * public int getAge(); + * public Address getAddress(); + * public boolean deceased; + * } + * + * public class Address { + * public String getStreet(); + * public String getCity(); + * public String getState(); + * public int getZip(); + * } + * + * Person p = new Person("John Doe", 23, "123 Main St", "Anywhere", "NY", 12345, false); + * + * // Produces "(name='John Doe',age=23,address=(street='123 Main St',city=Anywhere,state=NY,zip=12345),deceased=false)" + * String s = UonSerializer.DEFAULT.serialize(s); + *

+ */ +@Produces("text/uon") +public class UonSerializer extends WriterSerializer { + + /** Reusable instance of {@link UonSerializer}, all default settings. */ + public static final UonSerializer DEFAULT = new UonSerializer(PropertyStore.create()); + + /** Reusable instance of {@link UonSerializer.Readable}. */ + public static final UonSerializer DEFAULT_READABLE = new Readable(PropertyStore.create()); + + /** Reusable instance of {@link UonSerializer.Encoding}. */ + public static final UonSerializer DEFAULT_ENCODING = new Encoding(PropertyStore.create()); + + /** + * Equivalent to new UonSerializerBuilder().ws().build();. + */ + public static class Readable extends UonSerializer { + + /** + * Constructor. + * @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); + } + } + + /** + * Equivalent to new UonSerializerBuilder().encoding().build();. + */ + public static class Encoding extends UonSerializer { + + /** + * Constructor. + * @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); + } + } + + + private final UonSerializerContext ctx; + + /** + * Constructor. + * @param propertyStore The property store containing all the settings for this object. + */ + public UonSerializer(PropertyStore propertyStore) { + super(propertyStore); + this.ctx = createContext(UonSerializerContext.class); + } + + @Override /* CoreObject */ + public UonSerializerBuilder builder() { + 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. null 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(); + 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.cr(depth-1); + out.append(')'); + + return out; + } + + private SerializerWriter serializeBeanMap(UonSerializerSession session, UonWriter out, BeanMap m, String typeName) throws Exception { + int depth = session.getIndent(); + + 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.addBeanGetterWarning(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.cr(depth-1); + 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); + + out.append('@').append('('); + + int depth = session.getIndent(); + + for (Iterator i = c.iterator(); i.hasNext();) { + out.cr(depth); + serializeAnything(session, out, i.next(), elementType, "", null); + if (i.hasNext()) + out.append(','); + } + + if (c.size() > 0) + out.cr(depth-1); + out.append(')'); + + return out; + } + + + //-------------------------------------------------------------------------------- + // Entry point methods + //-------------------------------------------------------------------------------- + + @Override /* Serializer */ + public UonSerializerSession createSession(Object output, ObjectMap op, Method javaMethod, Locale locale, TimeZone timeZone, MediaType mediaType) { + return new UonSerializerSession(ctx, op, output, javaMethod, locale, timeZone, mediaType); + } + + @Override /* Serializer */ + protected void doSerialize(SerializerSession session, Object o) throws Exception { + UonSerializerSession s = (UonSerializerSession)session; + serializeAnything(s, s.getWriter(), o, null, "root", null); + } +}