http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/urlencoding/UonParser.java
----------------------------------------------------------------------
diff --git 
a/juneau-core/src/main/java/org/apache/juneau/urlencoding/UonParser.java 
b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UonParser.java
new file mode 100644
index 0000000..daeddf3
--- /dev/null
+++ b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UonParser.java
@@ -0,0 +1,829 @@
+/***************************************************************************************************************************
+ * 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.urlencoding;
+
+import static org.apache.juneau.urlencoding.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.
+ *
+ *
+ * <h6 class='topic'>Media types</h6>
+ * <p>
+ *     Handles <code>Content-Type</code> types: <code>text/uon</code>
+ *
+ *
+ * <h6 class='topic'>Description</h6>
+ * <p>
+ *     This parser uses a state machine, which makes it very fast and 
efficient.
+ *
+ *
+ * <h6 class='topic'>Configurable properties</h6>
+ * <p>
+ *     This class has the following properties associated with it:
+ * <ul>
+ *     <li>{@link UonParserContext}
+ *     <li>{@link ParserContext}
+ *     <li>{@link BeanContext}
+ * </ul>
+ *
+ *
+ * @author James Bognar ([email protected])
+ */
+@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().lock();
+
+       /** Reusable instance of {@link UonParser.Decoding}. */
+       public static final UonParser DEFAULT_DECODING = new Decoding().lock();
+
+       /** Reusable instance of {@link UonParser}, all default settings, 
whitespace-aware. */
+       public static final UonParser DEFAULT_WS_AWARE = new 
UonParser().setProperty(UON_whitespaceAware, true).lock();
+
+       // Characters that need to be preceeded with an escape character.
+       private static final AsciiSet escapedChars = new 
AsciiSet(",()~=$\u0001\u0002");
+
+       private static final char NUL='\u0000', AMP='\u0001', EQ='\u0002';  // 
Flags set in reader to denote & and = characters.
+
+       /**
+        * Equivalent to <code><jk>new</jk> 
UrlEncodingParser().setProperty(UonParserContext.<jsf>UON_decodeChars</jsf>,<jk>true</jk>);</code>.
+        */
+       public static class Decoding extends UonParser {
+               /** Constructor */
+               public Decoding() {
+                       setProperty(UON_decodeChars, true);
+               }
+       }
+
+       /**
+        * Workhorse method.
+        *
+        * @param session The parser context for this parse.
+        * @param nt 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.
+        * @return The parsed object.
+        * @throws Exception
+        */
+       protected <T> T parseAnything(UonParserSession session, ClassMeta<T> 
nt, ParserReader r, Object outer, boolean isUrlParamValue) throws Exception {
+
+               BeanContext bc = session.getBeanContext();
+               if (nt == null)
+                       nt = (ClassMeta<T>)object();
+               PojoTransform<T,Object> transform = 
(PojoTransform<T,Object>)nt.getPojoTransform();
+               ClassMeta<?> ft = nt.getTransformedClassMeta();
+
+               Object o = null;
+
+               // Parse type flag '$x'
+               char flag = readFlag(session, r, (char)0);
+
+               int c = r.peek();
+
+               if (c == -1 || c == AMP) {
+                       // If parameter is blank and it's an array or 
collection, return an empty list.
+                       if (ft.isArray() || ft.isCollection())
+                               o = ft.newInstance();
+                       else if (ft.isString() || ft.isObject())
+                               o = "";
+                       else if (ft.isPrimitive())
+                               o = ft.getPrimitiveDefault();
+                       // Otherwise, leave null.
+               } else if (ft.isObject()) {
+                       if (flag == 0 || flag == 's') {
+                               o = parseString(session, r, isUrlParamValue);
+                       } else if (flag == 'b') {
+                               o = parseBoolean(session, r);
+                       } else if (flag == 'n') {
+                               o = parseNumber(session, r, null);
+                       } else if (flag == 'o') {
+                               ObjectMap m = new ObjectMap(bc);
+                               parseIntoMap(session, r, m, string(), object());
+                               o = m.cast();
+                       } else if (flag == 'a') {
+                               Collection l = new ObjectList(bc);
+                               o = parseIntoCollection(session, r, l, 
ft.getElementType(), isUrlParamValue);
+                       } else {
+                               throw new ParseException(session, "Unexpected 
flag character ''{0}''.", flag);
+                       }
+               } else if (ft.isBoolean()) {
+                       o = parseBoolean(session, r);
+               } else if (ft.isCharSequence()) {
+                       o = parseString(session, r, isUrlParamValue);
+               } else if (ft.isChar()) {
+                       String s = parseString(session, r, isUrlParamValue);
+                       o = s == null ? null : s.charAt(0);
+               } else if (ft.isNumber()) {
+                       o = parseNumber(session, r, (Class<? extends 
Number>)ft.getInnerClass());
+               } else if (ft.isMap()) {
+                       Map m = (ft.canCreateNewInstance(outer) ? 
(Map)ft.newInstance(outer) : new ObjectMap(bc));
+                       o = parseIntoMap(session, r, m, ft.getKeyType(), 
ft.getValueType());
+               } else if (ft.isCollection()) {
+                       if (flag == 'o') {
+                               ObjectMap m = new ObjectMap(bc);
+                               parseIntoMap(session, r, m, string(), object());
+                               // Handle case where it's a collection, but 
serialized as a map with a _class or _value key.
+                               if (m.containsKey("_class") || 
m.containsKey("_value"))
+                                       o = m.cast();
+                               // Handle case where it's a collection, but 
only a single value was specified.
+                               else {
+                                       Collection l = 
(ft.canCreateNewInstance(outer) ? (Collection)ft.newInstance(outer) : new 
ObjectList(bc));
+                                       l.add(m.cast(ft.getElementType()));
+                                       o = l;
+                               }
+                       } else {
+                               Collection l = (ft.canCreateNewInstance(outer) 
? (Collection)ft.newInstance(outer) : new ObjectList(bc));
+                               o = parseIntoCollection(session, r, l, 
ft.getElementType(), isUrlParamValue);
+                       }
+               } else if (ft.canCreateNewInstanceFromObjectMap(outer)) {
+                       ObjectMap m = new ObjectMap(bc);
+                       parseIntoMap(session, r, m, string(), object());
+                       o = ft.newInstanceFromObjectMap(outer, m);
+               } else if (ft.canCreateNewBean(outer)) {
+                       BeanMap m = bc.newBeanMap(outer, ft.getInnerClass());
+                       m = parseIntoBeanMap(session, r, m);
+                       o = m == null ? null : m.getBean();
+               } else if (ft.canCreateNewInstanceFromString(outer)) {
+                       String s = parseString(session, r, isUrlParamValue);
+                       if (s != null)
+                               o = ft.newInstanceFromString(outer, s);
+               } else if (ft.canCreateNewInstanceFromNumber(outer)) {
+                       o = ft.newInstanceFromNumber(outer, 
parseNumber(session, r, ft.getNewInstanceFromNumberClass()));
+               } else if (ft.isArray()) {
+                       if (flag == 'o') {
+                               ObjectMap m = new ObjectMap(bc);
+                               parseIntoMap(session, r, m, string(), object());
+                               // Handle case where it's an array, but 
serialized as a map with a _class or _value key.
+                               if (m.containsKey("_class") || 
m.containsKey("_value"))
+                                       o = m.cast();
+                               // Handle case where it's an array, but only a 
single value was specified.
+                               else {
+                                       ArrayList l = new ArrayList(1);
+                                       l.add(m.cast(ft.getElementType()));
+                                       o = bc.toArray(ft, l);
+                               }
+                       } else {
+                               ArrayList l = 
(ArrayList)parseIntoCollection(session, r, new ArrayList(), 
ft.getElementType(), isUrlParamValue);
+                               o = bc.toArray(ft, l);
+                       }
+               } else if (flag == 'o') {
+                       // It could be a non-bean with _class attribute.
+                       ObjectMap m = new ObjectMap(bc);
+                       parseIntoMap(session, r, m, string(), object());
+                       if (m.containsKey("_class"))
+                               o = m.cast();
+                       else
+                               throw new ParseException(session, "Class 
''{0}'' could not be instantiated.  Reason: ''{1}''", 
ft.getInnerClass().getName(), ft.getNotABeanReason());
+               } else {
+                       throw new ParseException(session, "Class ''{0}'' could 
not be instantiated.  Reason: ''{1}''", ft.getInnerClass().getName(), 
ft.getNotABeanReason());
+               }
+
+               if (transform != null && o != null)
+                       o = transform.normalize(o, nt);
+
+               if (outer != null)
+                       setParent(nt, o, outer);
+
+               return (T)o;
+       }
+
+       private <K,V> Map<K,V> parseIntoMap(UonParserSession session, 
ParserReader r, Map<K,V> m, ClassMeta<K> keyType, ClassMeta<V> valueType) 
throws Exception {
+
+               if (keyType == null)
+                       keyType = (ClassMeta<K>)string();
+
+               int c = r.read();
+               if (c == -1 || c == NUL || c == AMP)
+                       return null;
+               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 ((c == '\n' || c == '\r') && 
session.isWhitespaceAware())
+                                               skipSpace(r);
+                                       else {
+                                               r.unread();
+                                               Object attr = 
parseAttr(session, r, session.isDecodeChars());
+                                               currAttr = session.trim((attr 
== null ? null : session.getBeanContext().convertToType(attr, 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);
+                                               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 <E> Collection<E> parseIntoCollection(UonParserSession session, 
ParserReader r, Collection<E> l, ClassMeta<E> elementType, boolean 
isUrlParamValue) throws Exception {
+
+               int c = r.read();
+               if (c == -1 || c == NUL || c == AMP)
+                       return null;
+
+               // 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.");
+
+               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));
+                                                       r.read();
+                                               }
+                                               return l;
+                                       } else if ((c == '\n' || c == '\r') && 
session.isWhitespaceAware()) {
+                                               skipSpace(r);
+                                       } else {
+                                               l.add(parseAnything(session, 
elementType, r.unread(), l, false));
+                                               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 ((c == '\n' || c == '\r') && 
session.isWhitespaceAware()) {
+                                               skipSpace(r);
+                                       } else {
+                                               l.add(parseAnything(session, 
elementType, r.unread(), l, false));
+                                               state = S2;
+                                       }
+                               } else if (state == S2) {
+                                       if (c == ',') {
+                                               state = S1;
+                                       } else if ((c == '\n' || c == '\r') && 
session.isWhitespaceAware()) {
+                                               skipSpace(r);
+                                       } else if (c == AMP || c == -1) {
+                                               r.unread();
+                                               return l;
+                                       }
+                               }
+                       }
+               }
+
+               return null;  // Unreachable.
+       }
+
+       private <T> BeanMap<T> parseIntoBeanMap(UonParserSession session, 
ParserReader r, BeanMap<T> m) throws Exception {
+
+               int c = r.read();
+               if (c == -1 || c == NUL || c == AMP)
+                       return null;
+               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 ((c == '\n' || c == '\r') && 
session.isWhitespaceAware())
+                                               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("_class")) {
+                                                       BeanPropertyMeta pMeta 
= m.getPropertyMeta(currAttr);
+                                                       if (pMeta == null) {
+                                                               if 
(m.getMeta().isSubTyped()) {
+                                                                       
m.put(currAttr, "");
+                                                               } else {
+                                                                       
onUnknownProperty(session, currAttr, m, currAttrLine, currAttrCol);
+                                                               }
+                                                       } else {
+                                                               Object value = 
session.getBeanContext().convertToType("", pMeta.getClassMeta());
+                                                               pMeta.set(m, 
value);
+                                                       }
+                                               }
+                                               if (c == -1 || c == ')' || c == 
AMP)
+                                                       return m;
+                                               state = S1;
+                                       } else {
+                                               if (! 
currAttr.equals("_class")) {
+                                                       BeanPropertyMeta pMeta 
= m.getPropertyMeta(currAttr);
+                                                       if (pMeta == null) {
+                                                               if 
(m.getMeta().isSubTyped()) {
+                                                                       Object 
value = parseAnything(session, object(), r.unread(), m.getBean(false), false);
+                                                                       
m.put(currAttr, value);
+                                                               } else {
+                                                                       
onUnknownProperty(session, currAttr, m, currAttrLine, currAttrCol);
+                                                                       
parseAnything(session, object(), r.unread(), m.getBean(false), false); // 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);
+                                                               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.
+       }
+
+       Object parseAttr(UonParserSession session, ParserReader r, boolean 
encoded) throws Exception {
+               Object attr;
+               int c = r.peek();
+               if (c == '$') {
+                       char f = readFlag(session, r, (char)0);
+                       if (f == 'b')
+                               attr = parseBoolean(session, r);
+                       else if (f == 'n')
+                               attr = parseNumber(session, r, null);
+                       else
+                               attr = parseAttrName(session, r, encoded);
+               } else {
+                       attr = parseAttrName(session, r, encoded);
+               }
+               return attr;
+       }
+
+       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 -1 denoting the 
end of this string.
+
+               int c = r.peek();
+               if (c == '$')
+                       readFlag(session, r, 's');
+               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) {
+                                               if (c != -1)
+                                                       r.unread();
+                                               String s = r.getMarked();
+                                               return (s.equals("\u0000") ? 
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) {
+                                               if (c != -1)
+                                                       r.unread();
+                                               String s = r.getMarked();
+                                               return (s.equals("\u0000") ? 
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;
+       }
+
+       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.peek();
+               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 ((c == '\n' || c == '\r') && 
session.isWhitespaceAware()) {
+                               s = r.getMarked(0, -1);
+                               skipSpace(r);
+                               c = -1;
+                       }
+                       isInEscape = isInEscape(c, r, isInEscape);
+               }
+
+               return (s == null || s.equals("\u0000") ? 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 parenthesis.
+               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 {
+               readFlag(session, r, 'b');
+               String s = parseString(session, r, false);
+               if (s == 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<? extends Number> c) throws Exception {
+               readFlag(session, r, 'n');
+               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 {
+               int c = r.read();
+               if (c != -1)
+                       throw new ParseException(session, "Remainder after 
parse: ''{0}''.", (char)c);
+       }
+
+       /**
+        * Reads flag character from "$x(" construct if flag is present.
+        * Returns 0 if no flag is present.
+        */
+       static char readFlag(UonParserSession session, ParserReader r, char 
expected) throws Exception {
+               char c = (char)r.peek();
+               if (c == '$') {
+                       r.read();
+                       char f = (char)r.read();
+                       if (expected != 0 && f != expected)
+                               throw new ParseException(session, "Unexpected 
flag character: ''{0}''.  Expected ''{1}''.", f, expected);
+                       c = (char)r.peek();
+                       // Type flag must be followed by '('
+                       if (c != '(')
+                               throw new ParseException(session, "Unexpected 
character following flag: ''{0}''.", c);
+                       return f;
+               }
+               return 0;
+       }
+
+       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.read();
+               if (c == -1 || c == AMP)
+                       return null;
+               if (c != '(')
+                       throw new ParseException(session, "Expected '(' at 
beginning of args array.");
+
+               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);
+                               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 > ' ' || c <= 2) {
+                               r.unread();
+                               return;
+                       }
+               }
+       }
+
+       UonParserSession createParameterContext(Object input) {
+               return new UonParserSession(getContext(UonParserContext.class), 
getBeanContext(), input);
+       }
+
+       
//--------------------------------------------------------------------------------
+       // Overridden methods
+       
//--------------------------------------------------------------------------------
+
+       @Override /* Parser */
+       public UonParserSession createSession(Object input, ObjectMap 
properties, Method javaMethod, Object outer) {
+               return new UonParserSession(getContext(UonParserContext.class), 
getBeanContext(), input, properties, javaMethod, outer);
+       }
+
+       @Override /* Parser */
+       protected <T> T doParse(ParserSession session, ClassMeta<T> type) 
throws Exception {
+               UonParserSession s = (UonParserSession)session;
+               type = s.getBeanContext().normalizeClassMeta(type);
+               UonReader r = s.getReader();
+               T o = parseAnything(s, type, r, s.getOuter(), true);
+               validateEnd(s, r);
+               return o;
+       }
+
+       @Override /* ReaderParser */
+       protected <K,V> Map<K,V> doParseIntoMap(ParserSession session, Map<K,V> 
m, Type keyType, Type valueType) throws Exception {
+               UonParserSession s = (UonParserSession)session;
+               UonReader r = s.getReader();
+               readFlag(s, r, 'o');
+               m = parseIntoMap(s, r, m, 
s.getBeanContext().getClassMeta(keyType), 
s.getBeanContext().getClassMeta(valueType));
+               validateEnd(s, r);
+               return m;
+       }
+
+       @Override /* ReaderParser */
+       protected <E> Collection<E> doParseIntoCollection(ParserSession 
session, Collection<E> c, Type elementType) throws Exception {
+               UonParserSession s = (UonParserSession)session;
+               UonReader r = s.getReader();
+               readFlag(s, r, 'a');
+               c = parseIntoCollection(s, r, c, 
s.getBeanContext().getClassMeta(elementType), false);
+               validateEnd(s, r);
+               return c;
+       }
+
+       @Override /* ReaderParser */
+       protected Object[] doParseArgs(ParserSession session, ClassMeta<?>[] 
argTypes) throws Exception {
+               UonParserSession s = (UonParserSession)session;
+               UonReader r = s.getReader();
+               readFlag(s, r, 'a');
+               Object[] a = parseArgs(s, r, argTypes);
+               return a;
+       }
+
+       @Override /* Parser */
+       public UonParser setProperty(String property, Object value) throws 
LockedException {
+               super.setProperty(property, value);
+               return this;
+       }
+
+       @Override /* CoreApi */
+       public UonParser setProperties(ObjectMap properties) throws 
LockedException {
+               super.setProperties(properties);
+               return this;
+       }
+
+       @Override /* CoreApi */
+       public UonParser addNotBeanClasses(Class<?>...classes) throws 
LockedException {
+               super.addNotBeanClasses(classes);
+               return this;
+       }
+
+       @Override /* CoreApi */
+       public UonParser addTransforms(Class<?>...classes) throws 
LockedException {
+               super.addTransforms(classes);
+               return this;
+       }
+
+       @Override /* CoreApi */
+       public <T> UonParser addImplClass(Class<T> interfaceClass, Class<? 
extends T> implClass) throws LockedException {
+               super.addImplClass(interfaceClass, implClass);
+               return this;
+       }
+
+       @Override /* CoreApi */
+       public UonParser setClassLoader(ClassLoader classLoader) throws 
LockedException {
+               super.setClassLoader(classLoader);
+               return this;
+       }
+
+       @Override /* Lockable */
+       public UonParser lock() {
+               super.lock();
+               return this;
+       }
+
+       @Override /* Lockable */
+       public UonParser clone() {
+               try {
+                       UonParser c = (UonParser)super.clone();
+                       return c;
+               } catch (CloneNotSupportedException e) {
+                       throw new RuntimeException(e); // Shouldn't happen
+               }
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/urlencoding/UonParserContext.java
----------------------------------------------------------------------
diff --git 
a/juneau-core/src/main/java/org/apache/juneau/urlencoding/UonParserContext.java 
b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UonParserContext.java
new file mode 100644
index 0000000..614cffc
--- /dev/null
+++ 
b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UonParserContext.java
@@ -0,0 +1,69 @@
+/***************************************************************************************************************************
+ * 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.urlencoding;
+
+import org.apache.juneau.*;
+import org.apache.juneau.parser.*;
+
+/**
+ * Configurable properties on the {@link UonParser} class.
+ * <p>
+ * Context properties are set by calling {@link 
ContextFactory#setProperty(String, Object)} on the context factory
+ * returned {@link CoreApi#getContextFactory()}.
+ * <p>
+ * The following convenience methods are also provided for setting context 
properties:
+ * <ul>
+ *     <li>{@link UonParser#setProperty(String,Object)}
+ *     <li>{@link UonParser#setProperties(ObjectMap)}
+ *     <li>{@link UonParser#addNotBeanClasses(Class[])}
+ *     <li>{@link UonParser#addTransforms(Class[])}
+ *     <li>{@link UonParser#addImplClass(Class,Class)}
+ * </ul>
+ * <p>
+ * See {@link ContextFactory} for more information about context properties.
+ *
+ * @author James Bognar ([email protected])
+ */
+public class UonParserContext extends ParserContext {
+
+       /**
+        * Decode <js>"%xx"</js> sequences. ({@link Boolean}, 
default=<jk>false</jk> for {@link UonParser}, <jk>true</jk> for {@link 
UrlEncodingParser}).
+        * <p>
+        * Specify <jk>true</jk> if URI encoded characters should be decoded, 
<jk>false</jk>
+        *      if they've already been decoded before being passed to this 
parser.
+        */
+       public static final String UON_decodeChars = "UonParser.decodeChars";
+
+       /**
+        * Expect input to contain readable whitespace characters from using 
the {@link UonSerializerContext#UON_useWhitespace} setting ({@link Boolean}, 
default=<jk>false</jk>).
+        */
+       public static final String UON_whitespaceAware = 
"UonParser.whitespaceAware";
+
+
+       final boolean
+               decodeChars,
+               whitespaceAware;
+
+       /**
+        * Constructor.
+        * <p>
+        * Typically only called from {@link ContextFactory#getContext(Class)}.
+        *
+        * @param cf The factory that created this context.
+        */
+       public UonParserContext(ContextFactory cf) {
+               super(cf);
+               this.decodeChars = cf.getProperty(UON_decodeChars, 
boolean.class, false);
+               this.whitespaceAware = cf.getProperty(UON_whitespaceAware, 
boolean.class, false);
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/urlencoding/UonParserSession.java
----------------------------------------------------------------------
diff --git 
a/juneau-core/src/main/java/org/apache/juneau/urlencoding/UonParserSession.java 
b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UonParserSession.java
new file mode 100644
index 0000000..4e7398b
--- /dev/null
+++ 
b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UonParserSession.java
@@ -0,0 +1,129 @@
+/***************************************************************************************************************************
+ * 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.urlencoding;
+
+import static org.apache.juneau.urlencoding.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}.
+ * <p>
+ * This class is NOT thread safe.  It is meant to be discarded after one-time 
use.
+ *
+ * @author James Bognar ([email protected])
+ */
+public class UonParserSession extends ParserSession {
+
+       private final boolean decodeChars, whitespaceAware;
+       private UonReader reader;
+
+       /**
+        * 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 beanContext The bean context being used.
+        * @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.
+        */
+       public UonParserSession(UonParserContext ctx, BeanContext beanContext, 
Object input, ObjectMap op, Method javaMethod, Object outer) {
+               super(ctx, beanContext, input, op, javaMethod, outer);
+               if (op == null || op.isEmpty()) {
+                       decodeChars = ctx.decodeChars;
+                       whitespaceAware = ctx.whitespaceAware;
+               } else {
+                       decodeChars = op.getBoolean(UON_decodeChars, 
ctx.decodeChars);
+                       whitespaceAware = op.getBoolean(UON_whitespaceAware, 
ctx.whitespaceAware);
+               }
+       }
+
+       /**
+        * Create a specialized parser session for parsing URL parameters.
+        * <p>
+        * 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 beanContext The bean context being used.
+        * @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, BeanContext beanContext, 
Object input) {
+               super(ctx, beanContext, input, null, null, null);
+               decodeChars = false;
+               whitespaceAware = ctx.whitespaceAware;
+       }
+
+       /**
+        * 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;
+       }
+
+       /**
+        * Returns the {@link UonParserContext#UON_whitespaceAware} setting 
value for this session.
+        *
+        * @return The {@link UonParserContext#UON_whitespaceAware} setting 
value for this session.
+        */
+       public final boolean isWhitespaceAware() {
+               return whitespaceAware;
+       }
+
+       @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<String,Object> getLastLocation() {
+               Map<String,Object> 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/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/urlencoding/UonReader.java
----------------------------------------------------------------------
diff --git 
a/juneau-core/src/main/java/org/apache/juneau/urlencoding/UonReader.java 
b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UonReader.java
new file mode 100644
index 0000000..27a077a
--- /dev/null
+++ b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UonReader.java
@@ -0,0 +1,197 @@
+/***************************************************************************************************************************
+ * 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.urlencoding;
+
+import java.io.*;
+
+import org.apache.juneau.parser.*;
+
+/**
+ * Same functionality as {@link ParserReader} except automatically decoded 
<code>%xx</code> escape sequences.
+ * <p>
+ * Escape sequences are assumed to be encoded UTF-8.  Extended Unicode 
(&gt;\u10000) is supported.
+ * <p>
+ * If decoding is enabled, the following character replacements occur so that 
boundaries are not lost:
+ * <ul>
+ *     <li><js>'&'</js> -&gt; <js>'\u0001'</js>
+ *     <li><js>'='</js> -&gt; <js>'\u0002'</js>
+ * </ul>
+ *
+ * @author James Bognar ([email protected])
+ */
+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 <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}).
+        *
+        * @param r The Reader being wrapped.
+        * @param decodeChars If <jk>true</jk>, decode <code>%xx</code> 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/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/urlencoding/UonSerializer.java
----------------------------------------------------------------------
diff --git 
a/juneau-core/src/main/java/org/apache/juneau/urlencoding/UonSerializer.java 
b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UonSerializer.java
new file mode 100644
index 0000000..df94810
--- /dev/null
+++ b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UonSerializer.java
@@ -0,0 +1,498 @@
+/***************************************************************************************************************************
+ * 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.urlencoding;
+
+import static org.apache.juneau.serializer.SerializerContext.*;
+import static org.apache.juneau.urlencoding.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).
+ *
+ *
+ * <h6 class='topic'>Media types</h6>
+ * <p>
+ *     Handles <code>Accept</code> types: <code>text/uon</code>
+ * <p>
+ *     Produces <code>Content-Type</code> types: <code>text/uon</code>
+ *
+ *
+ * <h6 class='topic'>Description</h6>
+ * <p>
+ *     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.
+ *
+ *
+ * <h6 class='topic'>Configurable properties</h6>
+ * <p>
+ *     This class has the following properties associated with it:
+ * <ul>
+ *     <li>{@link UonSerializerContext}
+ *     <li>{@link BeanContext}
+ * </ul>
+ * <p>
+ *     The following shows a sample object defined in Javascript:
+ * </p>
+ * <p class='bcode'>
+ *     {
+ *             id: 1,
+ *             name: <js>'John Smith'</js>,
+ *             uri: <js>'http://sample/addressBook/person/1'</js>,
+ *             addressBookUri: <js>'http://sample/addressBook'</js>,
+ *             birthDate: <js>'1946-08-12T00:00:00Z'</js>,
+ *             otherIds: <jk>null</jk>,
+ *             addresses: [
+ *                     {
+ *                             uri: 
<js>'http://sample/addressBook/address/1'</js>,
+ *                             personUri: 
<js>'http://sample/addressBook/person/1'</js>,
+ *                             id: 1,
+ *                             street: <js>'100 Main Street'</js>,
+ *                             city: <js>'Anywhereville'</js>,
+ *                             state: <js>'NY'</js>,
+ *                             zip: 12345,
+ *                             isCurrent: <jk>true</jk>,
+ *                     }
+ *             ]
+ *     }
+ * </p>
+ * <p>
+ *     Using the "strict" syntax defined in this document, the equivalent
+ *             UON notation would be as follows:
+ * </p>
+ * <p class='bcode'>
+ *     $o(
+ *             <xa>id</xa>=$n(<xs>1</xs>),
+ *             <xa>name</xa>=<xs>John+Smith</xs>,
+ *             <xa>uri</xa>=<xs>http://sample/addressBook/person/1</xs>,
+ *             <xa>addressBookUri</xa>=<xs>http://sample/addressBook</xs>,
+ *             <xa>birthDate</xa>=<xs>1946-08-12T00:00:00Z</xs>,
+ *             <xa>otherIds</xa>=<xs>%00</xs>,
+ *             <xa>addresses</xa>=$a(
+ *                     $o(
+ *                             
<xa>uri</xa>=<xs>http://sample/addressBook/address/1</xs>,
+ *                             
<xa>personUri</xa>=<xs>http://sample/addressBook/person/1</xs>,
+ *                             <xa>id</xa>=$n(<xs>1</xs>),
+ *                             <xa>street</xa>=<xs>100+Main+Street</xs>,
+ *                             <xa>city</xa>=<xs>Anywhereville</xs>,
+ *                             <xa>state</xa>=<xs>NY</xs>,
+ *                             <xa>zip</xa>=$n(<xs>12345</xs>),
+ *                             <xa>isCurrent</xa>=$b(<xs>true</xs>)
+ *                     )
+ *             )
+ *     )
+ * </p>
+ * <p>
+ *     A secondary "lax" syntax is available when the data type of the
+ *             values are already known on the receiving end of the 
transmission:
+ * </p>
+ * <p class='bcode'>
+ *     (
+ *             <xa>id</xa>=<xs>1</xs>,
+ *             <xa>name</xa>=<xs>John+Smith</xs>,
+ *             <xa>uri</xa>=<xs>http://sample/addressBook/person/1</xs>,
+ *             <xa>addressBookUri</xa>=<xs>http://sample/addressBook</xs>,
+ *             <xa>birthDate</xa>=<xs>1946-08-12T00:00:00Z</xs>,
+ *             <xa>otherIds</xa>=<xs>%00</xs>,
+ *             <xa>addresses</xa>=(
+ *                     (
+ *                             
<xa>uri</xa>=<xs>http://sample/addressBook/address/1</xs>,
+ *                             
<xa>personUri</xa>=<xs>http://sample/addressBook/person/1</xs>,
+ *                             <xa>id</xa>=<xs>1</xs>,
+ *                             <xa>street</xa>=<xs>100+Main+Street</xs>,
+ *                             <xa>city</xa>=<xs>Anywhereville</xs>,
+ *                             <xa>state</xa>=<xs>NY</xs>,
+ *                             <xa>zip</xa>=<xs>12345</xs>,
+ *                             <xa>isCurrent</xa>=<xs>true</xs>
+ *                     )
+ *             )
+ *     )
+ * </p>
+ *
+ *
+ * <h6 class='topic'>Examples</h6>
+ * <p class='bcode'>
+ *     <jc>// Serialize a Map</jc>
+ *     Map m = <jk>new</jk> 
ObjectMap(<js>"{a:'b',c:1,d:false,e:['f',1,false],g:{h:'i'}}"</js>);
+ *
+ *     <jc>// Serialize to value equivalent to JSON.</jc>
+ *     <jc>// Produces 
"$o(a=b,c=$n(1),d=$b(false),e=$a(f,$n(1),$b(false)),g=$o(h=i))"</jc>
+ *     String s = UonSerializer.<jsf>DEFAULT</jsf>.serialize(s);
+ *
+ *     <jc>// Serialize to simplified value (for when data type is already 
known by receiver).</jc>
+ *     <jc>// Produces "(a=b,c=1,d=false,e=(f,1,false),g=(h=i))"</jc>
+ *     String s = UonSerializer.<jsf>DEFAULT_SIMPLE</jsf>.serialize(s);
+ *
+ *     <jc>// Serialize a bean</jc>
+ *     <jk>public class</jk> Person {
+ *             <jk>public</jk> Person(String s);
+ *             <jk>public</jk> String getName();
+ *             <jk>public int</jk> getAge();
+ *             <jk>public</jk> Address getAddress();
+ *             <jk>public boolean</jk> deceased;
+ *     }
+ *
+ *     <jk>public class</jk> Address {
+ *             <jk>public</jk> String getStreet();
+ *             <jk>public</jk> String getCity();
+ *             <jk>public</jk> String getState();
+ *             <jk>public int</jk> getZip();
+ *     }
+ *
+ *     Person p = <jk>new</jk> Person(<js>"John Doe"</js>, 23, <js>"123 Main 
St"</js>, <js>"Anywhere"</js>, <js>"NY"</js>, 12345, <jk>false</jk>);
+ *
+ *     <jc>// Produces "$o(name=John Doe,age=23,address=$o(street=123 Main 
St,city=Anywhere,state=NY,zip=$n(12345)),deceased=$b(false))"</jc>
+ *     String s = UonSerializer.<jsf>DEFAULT</jsf>.serialize(s);
+ *
+ *     <jc>// Produces "(name=John Doe,age=23,address=(street=123 Main 
St,city=Anywhere,state=NY,zip=12345),deceased=false)"</jc>
+ *     String s = UonSerializer.<jsf>DEFAULT_SIMPLE</jsf>.serialize(s);
+ * </p>
+ *
+ * @author James Bognar ([email protected])
+ */
+@Produces("text/uon")
+public class UonSerializer extends WriterSerializer {
+
+       /** Reusable instance of {@link UonSerializer}, all default settings. */
+       public static final UonSerializer DEFAULT = new UonSerializer().lock();
+
+       /** Reusable instance of {@link UonSerializer.Simple}. */
+       public static final UonSerializer DEFAULT_SIMPLE = new Simple().lock();
+
+       /** Reusable instance of {@link UonSerializer.Readable}. */
+       public static final UonSerializer DEFAULT_READABLE = new 
Readable().lock();
+
+       /** Reusable instance of {@link UonSerializer.Encoding}. */
+       public static final UonSerializer DEFAULT_ENCODING = new 
Encoding().lock();
+
+       /** Reusable instance of {@link UonSerializer.SimpleEncoding}. */
+       public static final UonSerializer DEFAULT_SIMPLE_ENCODING = new 
SimpleEncoding().lock();
+
+       /**
+        * Equivalent to <code><jk>new</jk> 
UonSerializer().setProperty(UonSerializerContext.<jsf>UON_simpleMode</jsf>,<jk>true</jk>);</code>.
+        */
+       @Produces(value={"text/uon-simple"},contentType="text/uon")
+       public static class Simple extends UonSerializer {
+               /** Constructor */
+               public Simple() {
+                       setProperty(UON_simpleMode, true);
+               }
+       }
+
+       /**
+        * Equivalent to <code><jk>new</jk> 
UonSerializer().setProperty(UonSerializerContext.<jsf>UON_useWhitespace</jsf>,<jk>true</jk>);</code>.
+        */
+       public static class Readable extends UonSerializer {
+               /** Constructor */
+               public Readable() {
+                       setProperty(UON_useWhitespace, true);
+                       setProperty(SERIALIZER_useIndentation, true);
+               }
+       }
+
+       /**
+        * Equivalent to <code><jk>new</jk> 
UonSerializer().setProperty(UonSerializerContext.<jsf>UON_encodeChars</jsf>,<jk>true</jk>);</code>.
+        */
+       public static class Encoding extends UonSerializer {
+               /** Constructor */
+               public Encoding() {
+                       setProperty(UON_encodeChars, true);
+               }
+       }
+
+       /**
+        * Equivalent to <code><jk>new</jk> 
UonSerializer().setProperty(UonSerializerContext.<jsf>UON_simpleMode</jsf>,<jk>true</jk>).setProperty(UonSerializerContext.<jsf>UON_encodeChars</jsf>,<jk>true</jk>);</code>.
+        */
+       @Produces(value={"text/uon-simple"},contentType="text/uon")
+       public static class SimpleEncoding extends UonSerializer {
+               /** Constructor */
+               public SimpleEncoding() {
+                       setProperty(UON_simpleMode, true);
+                       setProperty(UON_encodeChars, true);
+               }
+       }
+
+
+       /**
+        * 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.
+        * @param quoteEmptyStrings <jk>true</jk> if this is the first entry in 
an array.
+        * @param isTop If we haven't recursively called this method.
+        *
+        * @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, boolean 
quoteEmptyStrings, boolean isTop) throws Exception {
+               BeanContext bc = session.getBeanContext();
+
+               if (o == null) {
+                       out.appendObject(null, false, false, isTop);
+                       return out;
+               }
+
+               if (eType == null)
+                       eType = object();
+
+               boolean addClassAttr;           // Add "_class" attribute to 
element?
+               ClassMeta<?> aType;                     // The actual type
+               ClassMeta<?> gType;                     // The generic type
+
+               aType = session.push(attrName, o, eType);
+               boolean isRecursion = aType == null;
+
+               // Handle recursion
+               if (aType == null) {
+                       o = null;
+                       aType = object();
+               }
+
+               gType = aType.getTransformedClassMeta();
+               addClassAttr = (session.isAddClassAttrs() && ! 
eType.equals(aType));
+
+               // Transform if necessary
+               PojoTransform transform = aType.getPojoTransform();             
                // The transform
+               if (transform != null) {
+                       o = transform.transform(o);
+
+                       // If the transform's getTransformedClass() method 
returns Object, we need to figure out
+                       // the actual type now.
+                       if (gType.isObject())
+                               gType = bc.getClassMetaForObject(o);
+               }
+
+               // '\0' characters are considered null.
+               if (o == null || (gType.isChar() && ((Character)o).charValue() 
== 0))
+                       out.appendObject(null, false, false, isTop);
+               else if (gType.hasToObjectMapMethod())
+                       serializeMap(session, out, gType.toObjectMap(o), eType);
+               else if (gType.isBean())
+                       serializeBeanMap(session, out, bc.forBean(o), 
addClassAttr);
+               else if (gType.isUri() || (pMeta != null && (pMeta.isUri() || 
pMeta.isBeanUri())))
+                       out.appendUri(o, isTop);
+               else if (gType.isMap()) {
+                       if (o instanceof BeanMap)
+                               serializeBeanMap(session, out, (BeanMap)o, 
addClassAttr);
+                       else
+                               serializeMap(session, out, (Map)o, eType);
+               }
+               else if (gType.isCollection()) {
+                       if (addClassAttr)
+                               serializeCollectionMap(session, out, 
(Collection)o, gType);
+                       else
+                               serializeCollection(session, out, (Collection) 
o, eType);
+               }
+               else if (gType.isArray()) {
+                       if (addClassAttr)
+                               serializeCollectionMap(session, out, 
toList(gType.getInnerClass(), o), gType);
+                       else
+                               serializeCollection(session, out, 
toList(gType.getInnerClass(), o), eType);
+               }
+               else {
+                       out.appendObject(o, quoteEmptyStrings, false, isTop);
+               }
+
+               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.startFlag('o');
+
+               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, 
session.isUseWhitespace(), false, false).append('=');
+                       serializeAnything(session, out, value, valueType, (key 
== null ? null : session.toString(key)), null, session.isUseWhitespace(), 
false);
+                       if (mapEntries.hasNext())
+                               out.append(',');
+               }
+
+               if (m.size() > 0)
+                       out.cr(depth-1);
+               out.append(')');
+
+               return out;
+       }
+
+       @SuppressWarnings({ "rawtypes" })
+       private SerializerWriter serializeCollectionMap(UonSerializerSession 
session, UonWriter out, Collection o, ClassMeta<?> type) throws Exception {
+               int i = session.getIndent();
+               out.startFlag('o').nl();
+               out.append(i, "_class=").appendObject(type, false, false, 
false).append(',').nl();
+               out.append(i, "items=");
+               session.indent++;
+               serializeCollection(session, out, o, type);
+               session.indent--;
+
+               if (o.size() > 0)
+                       out.cr(i-1);
+               out.append(')');
+
+               return out;
+       }
+
+       @SuppressWarnings({ "rawtypes" })
+       private SerializerWriter serializeBeanMap(UonSerializerSession session, 
UonWriter out, BeanMap<?> m, boolean addClassAttr) throws Exception {
+               int depth = session.getIndent();
+
+               out.startFlag('o');
+
+               boolean addComma = false;
+
+               for (BeanPropertyValue p : m.getValues(addClassAttr, 
session.isTrimNulls())) {
+                       BeanPropertyMeta pMeta = p.getMeta();
+
+                       String key = p.getName();
+                       Object value = p.getValue();
+                       Throwable t = p.getThrown();
+                       if (t != null)
+                               session.addBeanGetterWarning(pMeta, t);
+
+                       if (session.canIgnoreValue(pMeta.getClassMeta(), key, 
value))
+                               continue;
+
+                       if (addComma)
+                               out.append(',');
+
+                       out.cr(depth).appendObject(key, false, false, 
false).append('=');
+
+                       serializeAnything(session, out, value, 
pMeta.getClassMeta(), key, pMeta, false, false);
+
+                       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.startFlag('a');
+
+               int depth = session.getIndent();
+               boolean quoteEmptyString = (c.size() == 1 || 
session.isUseWhitespace());
+
+               for (Iterator i = c.iterator(); i.hasNext();) {
+                       out.cr(depth);
+                       serializeAnything(session, out, i.next(), elementType, 
"<iterator>", null, quoteEmptyString, false);
+                       if (i.hasNext())
+                               out.append(',');
+               }
+
+               if (c.size() > 0)
+                       out.cr(depth-1);
+               out.append(')');
+
+               return out;
+       }
+
+       
//--------------------------------------------------------------------------------
+       // Overridden methods
+       
//--------------------------------------------------------------------------------
+
+       @Override /* Serializer */
+       public UonSerializerSession createSession(Object output, ObjectMap 
properties, Method javaMethod) {
+               return new 
UonSerializerSession(getContext(UonSerializerContext.class), getBeanContext(), 
output, properties, javaMethod);
+       }
+
+       @Override /* Serializer */
+       protected void doSerialize(SerializerSession session, Object o) throws 
Exception {
+               UonSerializerSession s = (UonSerializerSession)session;
+               serializeAnything(s, s.getWriter(), o, null, "root", null, 
false, true);
+       }
+
+       @Override /* CoreApi */
+       public UonSerializer setProperty(String property, Object value) throws 
LockedException {
+               super.setProperty(property, value);
+               return this;
+       }
+
+       @Override /* CoreApi */
+       public UonSerializer setProperties(ObjectMap properties) throws 
LockedException {
+               super.setProperties(properties);
+               return this;
+       }
+
+       @Override /* CoreApi */
+       public UonSerializer addNotBeanClasses(Class<?>...classes) throws 
LockedException {
+               super.addNotBeanClasses(classes);
+               return this;
+       }
+
+       @Override /* CoreApi */
+       public UonSerializer addTransforms(Class<?>...classes) throws 
LockedException {
+               super.addTransforms(classes);
+               return this;
+       }
+
+       @Override /* CoreApi */
+       public <T> UonSerializer addImplClass(Class<T> interfaceClass, Class<? 
extends T> implClass) throws LockedException {
+               super.addImplClass(interfaceClass, implClass);
+               return this;
+       }
+
+       @Override /* CoreApi */
+       public UonSerializer setClassLoader(ClassLoader classLoader) throws 
LockedException {
+               super.setClassLoader(classLoader);
+               return this;
+       }
+
+       @Override /* Lockable */
+       public UonSerializer lock() {
+               super.lock();
+               return this;
+       }
+
+       @Override /* Lockable */
+       public UonSerializer clone() {
+               try {
+                       UonSerializer c = (UonSerializer)super.clone();
+                       return c;
+               } catch (CloneNotSupportedException e) {
+                       throw new RuntimeException(e); // Shouldn't happen
+               }
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/urlencoding/UonSerializerContext.java
----------------------------------------------------------------------
diff --git 
a/juneau-core/src/main/java/org/apache/juneau/urlencoding/UonSerializerContext.java
 
b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UonSerializerContext.java
new file mode 100644
index 0000000..f0d68fd
--- /dev/null
+++ 
b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UonSerializerContext.java
@@ -0,0 +1,127 @@
+/***************************************************************************************************************************
+ * 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.urlencoding;
+
+import org.apache.juneau.*;
+import org.apache.juneau.serializer.*;
+
+/**
+ * Configurable properties on the {@link UonSerializer} class.
+ * <p>
+ * Context properties are set by calling {@link 
ContextFactory#setProperty(String, Object)} on the context factory
+ * returned {@link CoreApi#getContextFactory()}.
+ * <p>
+ * The following convenience methods are also provided for setting context 
properties:
+ * <ul>
+ *     <li>{@link UonSerializer#setProperty(String,Object)}
+ *     <li>{@link UonSerializer#setProperties(ObjectMap)}
+ *     <li>{@link UonSerializer#addNotBeanClasses(Class[])}
+ *     <li>{@link UonSerializer#addTransforms(Class[])}
+ *     <li>{@link UonSerializer#addImplClass(Class,Class)}
+ * </ul>
+ * <p>
+ * See {@link ContextFactory} for more information about context properties.
+ *
+ * @author James Bognar ([email protected])
+ */
+public class UonSerializerContext extends SerializerContext {
+
+       /**
+        * Use simplified output ({@link Boolean}, default=<jk>false</jk>).
+        * <p>
+        * If <jk>true</jk>, type flags will not be prepended to values in most 
cases.
+        * <p>
+        * Use this setting if the data types of the values (e.g. 
object/array/boolean/number/string)
+        *      is known on the receiving end.
+        * <p>
+        * It should be noted that the default behavior produces a data 
structure that can
+        *      be losslessly converted into JSON, and any JSON can be 
losslessly represented
+        *      in a URL-encoded value.  However, this strict equivalency does 
not exist
+        *      when simple mode is used.
+        * <p>
+        * <table class='styled'>
+        *      <tr>
+        *              <th>Input (in JSON)</th>
+        *              <th>Normal mode output</th>
+        *              <th>Simple mode output</th>
+        *      </tr>
+        *      <tr>
+        *              <td class='code'>{foo:'bar',baz:'bing'}</td>
+        *              <td class='code'>$o(foo=bar,baz=bing)</td>
+        *              <td class='code'>(foo=bar,baz=bing)</td>
+        *      </tr>
+        *      <tr>
+        *              <td class='code'>{foo:{bar:'baz'}}</td>
+        *              <td class='code'>$o(foo=$o(bar=baz))</td>
+        *              <td class='code'>(foo=(bar=baz))</td>
+        *      </tr>
+        *      <tr>
+        *              <td class='code'>['foo','bar']</td>
+        *              <td class='code'>$a(foo,bar)</td>
+        *              <td class='code'>(foo,bar)</td>
+        *      </tr>
+        *      <tr>
+        *              <td class='code'>['foo',['bar','baz']]</td>
+        *              <td class='code'>$a(foo,$a(bar,baz))</td>
+        *              <td class='code'>(foo,(bar,baz))</td>
+        *      </tr>
+        *      <tr>
+        *              <td class='code'>true</td>
+        *              <td class='code'>$b(true)</td>
+        *              <td class='code'>true</td>
+        *      </tr>
+        *      <tr>
+        *              <td class='code'>123</td>
+        *              <td class='code'>$n(123)</td>
+        *              <td class='code'>123</td>
+        *      </tr>
+        * </table>
+        */
+       public static final String UON_simpleMode = "UonSerializer.simpleMode";
+
+       /**
+        * Use whitespace in output ({@link Boolean}, default=<jk>false</jk>).
+        * <p>
+        * If <jk>true</jk>, whitespace is added to the output to improve 
readability.
+        */
+       public static final String UON_useWhitespace = 
"UonSerializer.useWhitespace";
+
+       /**
+        * Encode non-valid URI characters to <js>"%xx"</js> constructs. 
({@link Boolean}, default=<jk>false</jk> for {@link UonSerializer}, 
<jk>true</jk> for {@link UrlEncodingSerializer}).
+        * <p>
+        * If <jk>true</jk>, non-valid URI characters will be converted to 
<js>"%xx"</js> sequences.
+        * Set to <jk>false</jk> if parameter value is being passed to some 
other code that will already
+        *      perform URL-encoding of non-valid URI characters.
+        */
+       public static final String UON_encodeChars = 
"UonSerializer.encodeChars";
+
+
+       final boolean
+               simpleMode,
+               useWhitespace,
+               encodeChars;
+
+       /**
+        * Constructor.
+        * <p>
+        * Typically only called from {@link ContextFactory#getContext(Class)}.
+        *
+        * @param cf The factory that created this context.
+        */
+       public UonSerializerContext(ContextFactory cf) {
+               super(cf);
+               simpleMode = cf.getProperty(UON_simpleMode, boolean.class, 
false);
+               useWhitespace = cf.getProperty(UON_useWhitespace, 
boolean.class, false);
+               encodeChars = cf.getProperty(UON_encodeChars, boolean.class, 
false);
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/urlencoding/UonSerializerSession.java
----------------------------------------------------------------------
diff --git 
a/juneau-core/src/main/java/org/apache/juneau/urlencoding/UonSerializerSession.java
 
b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UonSerializerSession.java
new file mode 100644
index 0000000..a227ae5
--- /dev/null
+++ 
b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UonSerializerSession.java
@@ -0,0 +1,92 @@
+/***************************************************************************************************************************
+ * 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.urlencoding;
+
+import static org.apache.juneau.urlencoding.UonSerializerContext.*;
+
+import java.lang.reflect.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.json.*;
+import org.apache.juneau.serializer.*;
+
+/**
+ * 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.
+ *
+ * @author James Bognar ([email protected])
+ */
+public class UonSerializerSession extends SerializerSession {
+
+       private final boolean simpleMode, useWhitespace, encodeChars;
+
+       /**
+        * 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 beanContext The bean context being used.
+        * @param output The output object.  See {@link 
JsonSerializerSession#getWriter()} for valid class types.
+        * @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.
+        */
+       protected UonSerializerSession(UonSerializerContext ctx, BeanContext 
beanContext, Object output, ObjectMap op, Method javaMethod) {
+               super(ctx, beanContext, output, op, javaMethod);
+               if (op == null || op.isEmpty()) {
+                       simpleMode = ctx.simpleMode;
+                       useWhitespace = ctx.useWhitespace;
+                       encodeChars = ctx.encodeChars;
+               } else {
+                       simpleMode = op.getBoolean(UON_simpleMode, 
ctx.simpleMode);
+                       useWhitespace = op.getBoolean(UON_useWhitespace, 
ctx.useWhitespace);
+                       encodeChars = op.getBoolean(UON_encodeChars, 
ctx.encodeChars);
+               }
+       }
+
+       @Override /* SerializerSession */
+       public final UonWriter getWriter() throws Exception {
+               Object output = getOutput();
+               if (output instanceof UonWriter)
+                       return (UonWriter)output;
+               return new UonWriter(this, super.getWriter(), useWhitespace, 
isSimpleMode(), isEncodeChars(), isTrimStrings(), getRelativeUriBase(), 
getAbsolutePathUriBase());
+       }
+
+       /**
+        * Returns the {@link UonSerializerContext#UON_useWhitespace} setting 
value for this session.
+        *
+        * @return The {@link UonSerializerContext#UON_useWhitespace} setting 
value for this session.
+        */
+       public final boolean isUseWhitespace() {
+               return useWhitespace;
+       }
+
+       /**
+        * Returns the {@link UonSerializerContext#UON_simpleMode} setting 
value for this session.
+        *
+        * @return The {@link UonSerializerContext#UON_simpleMode} setting 
value for this session.
+        */
+       public final boolean isSimpleMode() {
+               return simpleMode;
+       }
+
+       /**
+        * 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;
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/urlencoding/UonWriter.java
----------------------------------------------------------------------
diff --git 
a/juneau-core/src/main/java/org/apache/juneau/urlencoding/UonWriter.java 
b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UonWriter.java
new file mode 100644
index 0000000..e428ad6
--- /dev/null
+++ b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UonWriter.java
@@ -0,0 +1,275 @@
+/***************************************************************************************************************************
+ * 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.urlencoding;
+
+import java.io.*;
+
+import org.apache.juneau.internal.*;
+import org.apache.juneau.serializer.*;
+
+/**
+ * Specialized writer for serializing UON-encoded text.
+ * <p>
+ *     <b>Note:  This class is not intended for external use.</b>
+ *
+ * @author James Bognar ([email protected])
+ */
+public final class UonWriter extends SerializerWriter {
+
+       private final UonSerializerSession session;
+       private final boolean simpleMode, encodeChars;
+
+       // Characters that do not need to be URL-encoded in strings.
+       private static final AsciiSet unencodedChars = new 
AsciiSet("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789;/?:@-_.!*'$(),~=");
+
+       // Characters that do not need to be URL-encoded in attribute names.
+       // Identical to unencodedChars, but excludes '='.
+       private static final AsciiSet unencodedCharsAttrName = new 
AsciiSet("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789;/?:@-_.!*'$(),~");
+
+       // Characters that need to be preceeded with an escape character.
+       private static final AsciiSet escapedChars = new AsciiSet(",()~=");
+
+       // AsciiSet that maps no characters.
+       private static final AsciiSet emptyCharSet = new AsciiSet("");
+
+       private static char[] hexArray = "0123456789ABCDEF".toCharArray();
+
+       /**
+        * Constructor.
+        *
+        * @param session The session that created this writer.
+        * @param out The writer being wrapped.
+        * @param useIndentation If <jk>true</jk>, tabs will be used in output.
+        * @param simpleMode If <jk>true</jk>, type flags will not be generated 
in output.
+        * @param encodeChars If <jk>true</jk>, special characters should be 
encoded.
+        * @param trimStrings If <jk>true</jk>, strings should be trimmed 
before they're serialized.
+        * @param relativeUriBase The base (e.g. 
<js>https://localhost:9443/contextPath";</js>) for relative URIs (e.g. 
<js>"my/path"</js>).
+        * @param absolutePathUriBase The base (e.g. 
<js>https://localhost:9443";</js>) for relative URIs with absolute paths (e.g. 
<js>"/contextPath/my/path"</js>).
+        */
+       protected UonWriter(UonSerializerSession session, Writer out, boolean 
useIndentation, boolean simpleMode, boolean encodeChars, boolean trimStrings, 
String relativeUriBase, String absolutePathUriBase) {
+               super(out, useIndentation, false, trimStrings, '\'', 
relativeUriBase, absolutePathUriBase);
+               this.session = session;
+               this.simpleMode = simpleMode;
+               this.encodeChars = encodeChars;
+       }
+
+       /**
+        * Serializes the specified simple object as a UON string value.
+        *
+        * @param o The object being serialized.
+        * @param quoteEmptyStrings Special case where we're serializing an 
array containing an empty string.
+        * @param isTopAttrName If this is a top-level attribute name we're 
serializing.
+        * @param isTop If this is a top-level value we're serializing.
+        * @return This object (for method chaining).
+        * @throws IOException Should never happen.
+        */
+       protected UonWriter appendObject(Object o, boolean quoteEmptyStrings, 
boolean isTopAttrName, boolean isTop) throws IOException {
+
+               char typeFlag = 0;
+
+               if (o == null)
+                       o = "\u0000";
+               else if (o.equals("\u0000"))
+                       typeFlag = 's';
+
+               String s = session.toString(o);
+//             if (trimStrings)
+//                     s = s.trim();
+               if (s.isEmpty()) {
+                       if (quoteEmptyStrings)
+                               typeFlag = 's';
+               } else if (s.charAt(0) == '(' || s.charAt(0) == '$') {
+                       typeFlag = 's';
+               } else if (useIndentation && (s.indexOf('\n') != -1 || 
(s.charAt(0) <= ' ' && s.charAt(0) != 0))) {
+                       // Strings containing newline characters must always be 
quoted so that they're not confused with whitespace.
+                       // Also, strings starting with whitespace must be 
quoted so that the contents are not ignored when whitespace is ignored.
+                       typeFlag = 's';
+               } else if (! simpleMode) {
+                       if (o instanceof Boolean)
+                               typeFlag = 'b';
+                       else if (o instanceof Number)
+                               typeFlag = 'n';
+               }
+
+               if (typeFlag != 0)
+                       startFlag(typeFlag);
+
+               AsciiSet unenc = (isTopAttrName ? unencodedCharsAttrName : 
unencodedChars);
+               AsciiSet esc = (isTop && typeFlag == 0 ? emptyCharSet : 
escapedChars);
+
+               for (int i = 0; i < s.length(); i++) {
+                       char c = s.charAt(i);
+                       if (esc.contains(c))
+                               append('~');
+                       if ((!encodeChars) || unenc.contains(c))
+                               append(c);
+                       else {
+                               if (c == ' ')
+                                       append('+');
+                               else {
+                                       int p = s.codePointAt(i);
+                                       if (p < 0x0080)
+                                               appendHex(p);
+                                       else if (p < 0x0800) {
+                                               int p1=p>>>6;
+                                               
appendHex(p1+192).appendHex((p&63)+128);
+                                       } else if (p < 0x10000) {
+                                               int p1=p>>>6, p2=p1>>>6;
+                                               
appendHex(p2+224).appendHex((p1&63)+128).appendHex((p&63)+128);
+                                       } else {
+                                               i++;  // Two-byte 
codepoint...skip past surrogate pair lower byte.
+                                               int p1=p>>>6, p2=p1>>>6, 
p3=p2>>>6;
+                                               
appendHex(p3+240).appendHex((p2&63)+128).appendHex((p1&63)+128).appendHex((p&63)+128);
+                                       }
+                               }
+                       }
+               }
+
+               if (typeFlag != 0)
+                       append(')');
+
+               return this;
+       }
+
+       /**
+        * Prints <code>$f(</code> in normal mode, and <code>(</code> in simple 
mode.
+        *
+        * @param f The flag character.
+        * @return This object (for method chaining).
+        * @throws IOException
+        */
+       protected UonWriter startFlag(char f) throws IOException {
+               if (f != 's' && ! simpleMode)
+                       append('$').append(f);
+               append('(');
+               return this;
+       }
+
+       /**
+        * Prints out a two-byte %xx sequence for the given byte value.
+        */
+       private UonWriter appendHex(int b) throws IOException {
+               if (b > 255)
+                       throw new IOException("Invalid value passed to 
appendHex.  Must be in the range 0-255.  Value=" + b);
+               append('%').append(hexArray[b>>>4]).append(hexArray[b&0x0F]);
+               return this;
+       }
+
+       /**
+        * Appends a URI to the output.
+        *
+        * @param uri The URI to append to the output.
+        * @param isTop If this is a top-level value we're serializing.
+        * @return This object (for method chaining).
+        * @throws IOException
+        */
+       public SerializerWriter appendUri(Object uri, boolean isTop) throws 
IOException {
+               String s = uri.toString();
+               if (s.indexOf("://") == -1) {
+                       if (StringUtils.startsWith(s, '/')) {
+                               if (absolutePathUriBase != null)
+                                       append(absolutePathUriBase);
+                       } else {
+                               if (relativeUriBase != null) {
+                                       append(relativeUriBase);
+                                       if (! relativeUriBase.equals("/"))
+                                               append("/");
+
+                               }
+                       }
+               }
+               return appendObject(s, false, false, isTop);
+       }
+
+       
//--------------------------------------------------------------------------------
+       // Overridden methods
+       
//--------------------------------------------------------------------------------
+
+       @Override /* SerializerWriter */
+       public UonWriter cr(int depth) throws IOException {
+               super.cr(depth);
+               return this;
+       }
+
+       @Override /* SerializerWriter */
+       public UonWriter appendln(int indent, String text) throws IOException {
+               super.appendln(indent, text);
+               return this;
+       }
+
+       @Override /* SerializerWriter */
+       public UonWriter appendln(String text) throws IOException {
+               super.appendln(text);
+               return this;
+       }
+
+       @Override /* SerializerWriter */
+       public UonWriter append(int indent, String text) throws IOException {
+               super.append(indent, text);
+               return this;
+       }
+
+       @Override /* SerializerWriter */
+       public UonWriter append(int indent, char c) throws IOException {
+               super.append(indent, c);
+               return this;
+       }
+
+       @Override /* SerializerWriter */
+       public UonWriter q() throws IOException {
+               super.q();
+               return this;
+       }
+
+       @Override /* SerializerWriter */
+       public UonWriter i(int indent) throws IOException {
+               super.i(indent);
+               return this;
+       }
+
+       @Override /* SerializerWriter */
+       public UonWriter nl() throws IOException {
+               super.nl();
+               return this;
+       }
+
+       @Override /* SerializerWriter */
+       public UonWriter append(Object text) throws IOException {
+               super.append(text);
+               return this;
+       }
+
+       @Override /* SerializerWriter */
+       public UonWriter append(String text) throws IOException {
+               super.append(text);
+               return this;
+       }
+
+       @Override /* SerializerWriter */
+       public UonWriter appendIf(boolean b, String text) throws IOException {
+               super.appendIf(b, text);
+               return this;
+       }
+
+       @Override /* SerializerWriter */
+       public UonWriter appendIf(boolean b, char c) throws IOException {
+               super.appendIf(b, c);
+               return this;
+       }
+
+       @Override /* SerializerWriter */
+       public UonWriter append(char c) throws IOException {
+               super.append(c);
+               return this;
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingClassMeta.java
----------------------------------------------------------------------
diff --git 
a/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingClassMeta.java
 
b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingClassMeta.java
new file mode 100644
index 0000000..6667632
--- /dev/null
+++ 
b/juneau-core/src/main/java/org/apache/juneau/urlencoding/UrlEncodingClassMeta.java
@@ -0,0 +1,59 @@
+/***************************************************************************************************************************
+ * 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.urlencoding;
+
+import org.apache.juneau.internal.*;
+import org.apache.juneau.urlencoding.annotation.*;
+
+/**
+ * Metadata on classes specific to the URL-Encoding serializers and parsers 
pulled from the {@link UrlEncoding @UrlEncoding} annotation on the class.
+ *
+ * @author James Bognar ([email protected])
+ */
+public class UrlEncodingClassMeta {
+
+       private final UrlEncoding urlEncoding;
+       private final boolean expandedParams;
+
+       /**
+        * Constructor.
+        *
+        * @param c The class that this annotation is defined on.
+        */
+       public UrlEncodingClassMeta(Class<?> c) {
+               this.urlEncoding = 
ReflectionUtils.getAnnotation(UrlEncoding.class, c);
+               if (urlEncoding != null) {
+                       expandedParams = urlEncoding.expandedParams();
+               } else {
+                       expandedParams = false;
+               }
+       }
+
+       /**
+        * Returns the {@link UrlEncoding} annotation defined on the class.
+        *
+        * @return The value of the {@link UrlEncoding} annotation, or 
<jk>null</jk> if annotation is not specified.
+        */
+       protected UrlEncoding getAnnotation() {
+               return urlEncoding;
+       }
+
+       /**
+        * Returns the {@link UrlEncoding#expandedParams()} annotation defined 
on the class.
+        *
+        * @return The value of the {@link UrlEncoding#expandedParams()} 
annotation.
+        */
+       protected boolean isExpandedParams() {
+               return expandedParams;
+       }
+}

Reply via email to