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 (>\u10000) is supported. + * <p> + * If decoding is enabled, the following character replacements occur so that boundaries are not lost: + * <ul> + * <li><js>'&'</js> -> <js>'\u0001'</js> + * <li><js>'='</js> -> <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; + } +}
